-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow passing additional context to app()
#190
base: main
Are you sure you want to change the base?
Conversation
How does this differ from having
|
Nice, yeah the difference is that you need some way to get context from the point at which you call the app into the RPC call, basically the request might not be the only thing from the surrounding closure that you need to pass down. Using this in a Cloudflare Worker, the serverless handler looks something like this: interface Env {
exampleBinding: DurableObjectNamespace
}
const rpc = createTwirpServerless([...])
const handler: ExportedHandler<Env> = {
async fetch(request, env) {
const url = new URL(request.url)
const res = await rpc({
method: request.method,
body: new Uint8Array(await request.arrayBuffer()),
headers: Object.fromEntries(request.headers),
url: url.pathname,
})
return new Response(res.body, {
headers: new Headers(res.headers as any),
status: res.statusCode,
})
},
} That's translating the web fetch API / Cloudflare Worker request and response types into something that But there's an additional So after this PR, you could do something like this: const handler: ExportedHandler<Env> = {
async fetch(request, env) {
const url = new URL(request.url)
const res = await rpc({
method: request.method,
body: new Uint8Array(await request.arrayBuffer()),
headers: Object.fromEntries(request.headers),
url: url.pathname,
- })
+ }, {env})
return new Response(res.body, {
headers: new Headers(res.headers as any),
status: res.statusCode,
})
},
} And now The other immediate use-case is I might need to pass the "real" request down to a handler for processing, so I can access the original request class. That might be something like |
10d4c30
to
43d6254
Compare
315afeb
to
6454d7e
Compare
I think I might have fixed CI by installing |
Awesome, thank you for the example. I've been thinking about reworking interface Env {
exampleBinding: DurableObjectNamespace
}
const rpc = createTwirpServerless([...])
const handler: ExportedHandler<Env> = {
fetch(request, env) {
return rpc(request);
}
} Then I think we could leverage middleware by splatting request with interface Env {
exampleBinding: DurableObjectNamespace
}
const rpc = createTwirpServerless<Env>([...])
+ rpc.use((req, ctx, next) => {
+ ctx.env = req.env;
+ return next();
+ });
const handler: ExportedHandler<Env> = {
fetch(request, env) {
- return rpc(request);
+ return rpc({ ...request, env });
}
} What do you think about this? |
I think that could work, and actually I think that would work today, without needing to conform to the fetch spec: app.use(async (request, context, next) => {
const {env} = request as unknown as {env: Env}
context.env = env
return next()
})
async fetch(request, env) {
const res = await app({
method: request.method,
body: new Uint8Array(await request.arrayBuffer()),
headers: Object.fromEntries(request.headers),
url: url.pathname,
env,
} as InboundRequest)
...
} The difficulty with that approach is the type-casts - without them today, you get errors like By putting the extra context on another function parameter, you can let TypeScript infer the type, or you can specify the type you expect in the generic as a safety check: // now TypeScript will check that extraContext is of type ExtraContext
await app<ExtraContext>(request, extraContext) |
Yeah this would work today, you would need (roughly) the following types to make it work: // something like this (hopefully there is a first classed type)
import type { Request } from 'cloudflare';
// or this if not
type Request = Parameters<ExportedHandler['fetch']>[0];
interface Env {
exampleBinding: DurableObjectNamespace
}
interface Context {
env: Env;
}
const services = [...];
const rpc = createTwirpServerless<Request & Context, typeof services, Context>(
services
);
rpc.use((req, ctx, next) => {
ctx.env = req.env;
return next();
});
const handler: ExportedHandler<Env> = {
fetch(request, env) {
return rpc({ ...request, env });
}
} The types aren't as ergonomic as I would like though. The main problem is Typescript generic inference is all or nothing -- there is a long standing TS issue to partially apply generics and infer others. |
I'm open to this change. I'd like to look into a few refactors I have in mind to see how it fits into that broader context. Specifically:
|
@@ -17,6 +17,11 @@ jobs: | |||
- run: npm install | |||
- run: npm run lint | |||
- run: npm run package:build | |||
- uses: actions/setup-go@v3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we ran into a transient error -- I need to look into it, but the merge into main
passed, and this was the first failed check I've seen.
The clientcompat
binary is vendored. I'm surprised my macos
build (local) runs on ubuntu-latest
(ci), but here we are 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this was the first failed check I've seen
Ah, this is because the clientcompat
test doesn't run in CI, but only if the CODECOV_TOKEN
environment variable is present:
TwirpScript/src/integration-tests/clientcompat/src/test.ts
Lines 13 to 16 in 413f01e
// TODO: Run under GitHub CI | |
if (process.env.CODECOV_TOKEN) { | |
return; | |
} |
My PRs are coming from a forked repo, so GitHub isn't providing their builds access to secrets (a security feature), so the CODECOV_TOKEN
isn't getting set, hence the failure. 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤦 I forgot about that hack. Sorry about that -- thank you for debugging this.
This PR allows passing additional keys to context as the last argument to the
app()
returned fromcreateTwirpServer
orcreateTwirpServerless
. This allows you to pass external context into the RPC call at the point you invoke the app. I've implemented it for bothTwirpServer
andTwirpServerless
, but I think it's most useful forTwirpServerless
, where you are building a custom RPC server on top of TwirpScript:The types are such that the new generic parameter for
app<Context>()
should only appear if theextraContext
is provided. And the implementation is such that the extra context is spread into the context before TwirpScript's default context fields and before middleware run, so those existing fields should always have the values they have currently.