-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Bug/#3267 hapi upload #3268
Bug/#3267 hapi upload #3268
Conversation
This addresses broken file upload handling by moving `processFileUploads` and related checks to the GraphQL route.
@swashcap: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Meteor Contributor Agreement here: https://contribute.meteor.com/ |
Huh, seeing this error on the server from the un-ignored test:
The server is returning a |
Ah, I see: moving upload handling to the route handler in c63bcad ensures that hapi parses the request and sets Kinda a catch 22: |
Unfortunately, changing the content-type check here doesn't work:
…to something like this: - request.mime === 'multipart/form-data'
+ /^multipart\/form-data/.test(request.headers['content-type'])
It looks like hapi re-assigns We could change
From here: apollo-server/packages/apollo-server-core/src/runHttpQuery.ts Lines 199 to 204 in 10402c4
Which seems to indicate some request payload parsing problems on the hapi side. |
I'm looking into the idiomatic hapi way to handle this and ran across hapijs/hapi#586, which helpfully points to the |
It looks like |
BREAKING CHANGE: This alters the `options.route` configuration object for the POST route. This separates GET and POST routes to facilitate extra route parsing configuration for the POST route. (hapi throws an error for this configuration when the route handler serves both methods.) POST route handling now parses the request manually to correct multipart/form-data requests through graphql-upload, and manually parses requests with hapijs/subtext for non-form requests.
processFileUploads, | ||
} from 'apollo-server-core'; | ||
|
||
function handleFileUploads(uploadsConfig: FileUploadOptions) { |
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 logic/handling moved to the hapi plugin.
@@ -409,7 +409,7 @@ const port = 0; | |||
}); | |||
}); | |||
describe('file uploads', () => { | |||
xit('enabled uploads', async () => { | |||
it('enabled uploads', async () => { |
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.
Test the thing!
@@ -138,6 +111,7 @@ export class ApolloServer extends ApolloServerBase { | |||
: { | |||
cors: cors !== undefined ? cors : true, | |||
}, | |||
uploadsConfig: this.uploadsConfig, |
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.
Pass the upload configuration to the plugin for use within the route handler (see comments below).
path: options.path || '/graphql', | ||
vhost: options.vhost || undefined, | ||
options: options.route || {}, | ||
handler: async (request, h) => { |
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 handler function's body – with some query
assignment changes – is copied to the handler
function.
// The request is unparsed due to the 'POST' route's config. Use | ||
// Subtext, hapi's default parser, to parse the request. | ||
// https://github.com/hapijs/subtext | ||
const { payload } = await Subtext.parse(request.raw.req, null, { |
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.
Subtext does the route payload parsing in hapi: https://github.com/hapijs/hapi/blob/03ca9133d016945ecc8642cc73fa7b05864ec605/lib/route.js#L429
hapi users will already have this library installed.
path, | ||
vhost: options.vhost || undefined, | ||
handler, | ||
options: { |
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.
Here's where it gets a little tricky: This patch necessarily adjusts the options.route
behavior for the POST route (hapi throws when the options.payload
is applied to a GET route). This could be considered a breaking change.
What this does: prevents hapi from parsing the http.IncomingMessage
, and passes the raw stream to the route handler as a readable stream.
See:
options.payload.output
: https://hapi.dev/api/?v=18.3.2#route.options.payload.outputoptions.payload.parse
: https://hapi.dev/api/?v=18.3.2#route.options.payload.parse
This means the route handler must manually parse the payload (thus the use of subtext).
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.
Could you help me understand the breaking change aspect of this a bit more?
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.
Trying to recall what I did here 😅
This package's ApolloServer#applyMiddleware
has an optional route
configuration that's passed directly to the hapi plugin if it's defined. This PR introduces a change in how the hapi plugin's route handler accepts uploads by changing the route's options.
Before:
options: options.route || {}, |
After (abbreviated):
server.route([
{
method: 'GET',
// ...
options: options.route || {},
},
{
method: 'POST',
// ...
options: {
...options.route,
payload: {
output: 'stream',
parse: false,
},
}
}
])
Notice how a user's configuration for payload.options
isn't passed through to the route. This is a necessary change: graphql-upload
's processRequest
(via processFileUploads) expects an unread stream such that it can handle the parsing. This is the only hapi route.options.payload
config that doesn't read the incoming message (that I found, anway). Unfortunately, allowing a user to override this breaks uploads.
This modifies how apollo-server-hapi passes user config down to the underlying route: should it be marked as a breaking change?
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 guess we could change it to:
{
options: {
...options.route,
payload: {
+ ...(options.route || {}).payload,
output: 'stream',
parse: false,
}
}
}
to give users that extra control.
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.
Hmmm, it looks like users were never able to set route.payload
. I made a demo on Glitch to test this out: https://glitch.com/edit/#!/scratch-animantarx?path=server.js:44:4
Even with route.payload
set to empty object, hapi seems to error:
AssertionError [ERR_ASSERTION]: Cannot set payload settings on HEAD or GET request: GET /graphql
at new AssertionError (internal/assert.js:269:11)
# ...
The app never seems to start. If you "Remix" (fork in Glitch terms? first time trying it out) and find that "Logs" button in the bottom right you'll see the error.
I think the user was never able to pass payload, so this wouldn't be a breaking change at all 🎉
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 can confirm that I'm getting this error trying to override payload, because payload
should only be set on POST requests, and not HEAD or GET. But currently if you pass payload through as route options it will set on the GET route of /graphql too.
So yes it's not a breaking change
@@ -0,0 +1,18 @@ | |||
declare module 'subtext' { |
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.
@hapi/subtext
has types, but not subtext
. This should do until apollo-server upgrades to the @hapi
-scoped packages.
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'm wrong, there aren't official types: https://www.npmjs.com/package/@types/hapi__subtext. Perhaps I should contribute these back to the @types
repo.
Looks like the Node 6 tests fail:
What do you think about dropping Node.js 6 for apollo-server-hapi? |
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.
@swashcap I really appreciate your attention to fixing this and the comments you've left throughout. Very helpful!
Side-note / Relatedly: I will mention here that we will be unable to actually apply the more major version bump to Hapi to v18 that you opened in #3273. Others have tried, and we've tried to accommodate as well, but it's just not something that we can do within Apollo Server 2.x. Additionally, Apollo Server 3.x will remove the close coupling with Hapi (and all web frameworks) as a concern from this repository with #3184 being the intended replacement. As you're familiar with Hapi, your feedback appreciated on that issue would be very much appreciated, but the idea is that there would be:
- Many less baked-in middleware which can't be removed, swapped out or customized; and
- A slim adapter between the Apollo Server transport (which we will provide) and frameworks (e.g. Hapi). I hope this will work much better and free us from this close version coupling and the constraints it puts on both Apollo Server users and maintainers.
Back to the PR at hand though: I support this, since it fixes existing functionality.
And since, as you correctly point out, Hapi did in fact never officially test Node.js v6 going into its v17 release line (see the v17.0.0
tag's .travis.yml
), I would support dropping Node.js 6 tests in the same way we do already here:
(NODE_MAJOR_VERSION < 8 ? describe.skip : describe)('integration:Hapi', () => { |
If you can help me understand how the potential breaking change you mentioned below might be or not be a breaking change, I'd like to think about it briefly before finalizing this. If it is, in fact, a breaking a change, it may be the case that we simply cannot land this, but I hope that's not the case.
path, | ||
vhost: options.vhost || undefined, | ||
handler, | ||
options: { |
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.
Could you help me understand the breaking change aspect of this a bit more?
Had another look at the test failure:
Huh. Hapi doesn't transpile for older versions of Node.js. Probably means a little test matrix reworking, huh? |
I haven't had a chance to read through the proposal, but this sounds like a great idea: I believe hapi developers would appreciate owning some more boilerplate in exchange for the ability to keep up with the latest version. Personally, I've found that packages that wrap top-level dependencies in this fashion lead to some difficulty in upgrades, so I think this a good move from a maintenance standpoint. I'll read through it and mention it to my hapi-enthusiast coworkers to get some feedback. |
so this is not going to be merged and fixed anytime soon? |
works perfectly fine for me |
Been a while since I opened this PR. I believe we'd need some hapi-specific test matrix changes to get this working (remove NodeJS 6?). Looks like both 6 was turned off in c68c577. I'll have a look at rebasing. |
why was this closed? |
Is this just a case of disabling Node 6 tests and merging? Keen to use this fix as I'm running into the same errors with |
Hi guys! Thanks a lot! :) |
File upload integration was removed from Apollo Server in AS3 last year. Sorry for the delay! |
request.mime
not being set here:apollo-server/packages/apollo-server-hapi/src/ApolloServer.ts
Line 22 in 10402c4
onRequest
extensions.$ npx jest packages/apollo-server-hapi/src/__tests__/*.ts