Skip to content
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

Closed
wants to merge 3 commits into from

Conversation

swashcap
Copy link
Contributor

@swashcap swashcap commented Sep 8, 2019

This addresses broken file upload handling by moving
`processFileUploads` and related checks to the GraphQL route.
@apollo-cla
Copy link

@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/

@swashcap
Copy link
Contributor Author

swashcap commented Sep 8, 2019

Huh, seeing this error on the server from the un-ignored test:

Missing multipart field ‘operations’ (https://github.com/jaydenseric/graphql-multipart-request-spec).

The server is returning a 500. Interesting!

@swashcap
Copy link
Contributor Author

swashcap commented Sep 8, 2019

Ah, I see: moving upload handling to the route handler in c63bcad ensures that hapi parses the request and sets request.payload and request.mime, However, this reads the request stream entirely, allowing hapi to do some file upload handling. graphql-upload's processRequest needs the request stream to be unread!

Kinda a catch 22: request.mime isn't set unless the payload is parsed, but the payload can't be parsed for graphql-upload.

@swashcap
Copy link
Contributor Author

swashcap commented Sep 9, 2019

Unfortunately, changing the content-type check here doesn't work:

request.mime === 'multipart/form-data'

…to something like this:

- request.mime === 'multipart/form-data'
+ /^multipart\/form-data/.test(request.headers['content-type'])

payload isn't configurable, yet hapi mutates it here:

It looks like hapi re-assigns request.payload here: https://github.com/hapijs/hapi/blob/020065db61964f46e6af32293709f96ad61c6da7/lib/route.js#L401

We could change configurable to true, but then we run into another 500 error, this time:

POST body missing. Did you forget use body-parser middleware?

From here:

if (!httpRequest.query || Object.keys(httpRequest.query).length === 0) {
throw new HttpQueryError(
500,
'POST body missing. Did you forget use body-parser middleware?',
);
}

Which seems to indicate some request payload parsing problems on the hapi side.

@swashcap
Copy link
Contributor Author

swashcap commented Sep 9, 2019

I'm looking into the idiomatic hapi way to handle this and ran across hapijs/hapi#586, which helpfully points to the server.inject method, which references route.options.isInternal, a route configuration that registers the route for internal use only. I believe we can register an “internal only” route for multipart/form-data requests, then use request.setUrl to this in the server.ext('onRoute') handler. Going to give it a try.

@swashcap
Copy link
Contributor Author

swashcap commented Sep 9, 2019

It looks like server.inject isn't able to forward the original request. Merely passing the original request.payload yields the same "Missing multipart field ‘operations’ (https://github.com/jaydenseric/graphql-multipart-request-spec)." error, likely because the injection library, shot, isn't transforming the original payload into a proper http.IncomingMessage?. 😕

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) {
Copy link
Contributor Author

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 () => {
Copy link
Contributor Author

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,
Copy link
Contributor Author

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) => {
Copy link
Contributor Author

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, {
Copy link
Contributor Author

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: {
Copy link
Contributor Author

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:

This means the route handler must manually parse the payload (thus the use of subtext).

Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

@swashcap swashcap Sep 24, 2019

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 🎉

Copy link

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' {
Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@swashcap
Copy link
Contributor Author

swashcap commented Sep 9, 2019

Looks like the Node 6 tests fail:

    Details:

    /home/circleci/project/node_modules/subtext/lib/index.js:25
    exports.parse = async function (req, tap, options) {
                          ^^^^^^^^

    SyntaxError: Unexpected token function



      at ScriptTransformer._transformAndBuildScript (../../node_modules/@jest/core/node_modules/@jest/transform/build/ScriptTransformer.js:537:17)
      at ScriptTransformer.transform (../../node_modules/@jest/core/node_modules/@jest/transform/build/ScriptTransformer.js:579:25)
      at Object.<anonymous> (src/hapiApollo.ts:1850:42)

https://circleci.com/gh/apollographql/apollo-server/50505?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link

What do you think about dropping Node.js 6 for apollo-server-hapi? hapi@^17 only supports Node.js >=8.x.x: https://github.com/hapijs/hapi/blob/v17-commercial/.travis.yml

Copy link
Member

@abernix abernix left a 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:

  1. Many less baked-in middleware which can't be removed, swapped out or customized; and
  2. 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: {
Copy link
Member

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?

@swashcap
Copy link
Contributor Author

Had another look at the test failure:

    Details:

    /home/circleci/project/node_modules/subtext/lib/index.js:25
    exports.parse = async function (req, tap, options) {
                          ^^^^^^^^

    SyntaxError: Unexpected token function

https://circleci.com/gh/apollographql/apollo-server/50505?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link

Huh. Hapi doesn't transpile for older versions of Node.js. Probably means a little test matrix reworking, huh?

@swashcap
Copy link
Contributor Author

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.

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.

@wzrdtales
Copy link

so this is not going to be merged and fixed anytime soon?

@wzrdtales
Copy link

works perfectly fine for me @wzrdtales/apollo-server-hapi, using this module as replacement right now with this pull request and it works with hapi 19 perfectly fine

@swashcap
Copy link
Contributor Author

swashcap commented Jun 7, 2020

so this is not going to be merged and fixed anytime soon?

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.

@abernix abernix closed this Jun 24, 2020
@wzrdtales
Copy link

why was this closed?

@abernix abernix reopened this Jun 25, 2020
@abernix abernix changed the base branch from master to main June 25, 2020 09:04
@b-jam
Copy link

b-jam commented Aug 11, 2020

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 GET/HEAD payload mentioned above.

@TdyP
Copy link

TdyP commented Dec 18, 2020

Hi guys!
Any plan on merging this PR? I'm facing the Must provide query string issue and @wzrdtales fix works perfectly fine for me.

Thanks a lot! :)

@glasser
Copy link
Member

glasser commented Oct 4, 2022

File upload integration was removed from Apollo Server in AS3 last year. Sorry for the delay!

@glasser glasser closed this Oct 4, 2022
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 20, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants