-
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
Added file upload support to lambda. Supports multipart. Includes tests. #3676
Conversation
@k00k: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Apollo Contributor License Agreement here: https://contribute.apollographql.com/ |
@k00k I tried your pull request. I can upload image to s3 via |
@jacky-ttt I'll take a look. In the meantime, can you share your code? |
@jacky-ttt After some testing, I believe I was able to replicate the problem you were having. This is a guess, of course, because I don't know exactly all of the steps you did or didn't take. But I was able to upload locally and get "corrupted" files when sent through a real Lambda. I'm guessing it's likely due to needing to set API Gateway's Binary Media Types. In order to pass binary data via I've published an example repository that takes care of this for you if you deploy with the Serverless Framework. Otherwise, the README outlines how to do it manually. Please let me know if that does in fact solve your problem. If anyone else has other/better ideas on how to handle that, please feel free to chime in. But I believe it's likely out of the scope of this repo and thus shouldn't affect the merging of this PR. |
@k00k Thanks for the quick reply. Sorry for not share some code in the first place. The example repo is really helpful. Your suspicion is correct. What I miss is to use |
@abernix I'm very much interested in this feature. I was wondering how much time is usually needed for such a PR to be validated and released? Thanks in advance. And thank you so much for everything that is being done on Apollo Server ❤️ |
@abernix and @martijnwalraven is there anything we can do to help get this PR moving forward? |
I am still hitting the following error
Which is caused by supportUploads always returning false in the superclass: https://github.com/abcya/apollo-server/blob/fb60598286e9f45e55e3ccb197f1ce11b337014a/packages/apollo-server-core/src/ApolloServer.ts#L670 It looks like it should be as simple as adding the following in the ApolloServer subclass.
I mentioned it before here: I'm bit puzzled how it could be working at all unless it's still being executed against a very old version. |
You're correct, I've added the As to why I and a few others (and the automated tests) didn't have trouble with this originally, I agree, very weird. When you tested, were you supplying an In any case, please pull and give it a shot now, should be fine. |
@k00k Thanks for adding that. Yes, uploads option is being supplied to set maxFileSize. Using node v10.16.3 with typescript 3.7.4. I am actually copying apollo-server-lambda files into my project to get it working because I ran into some issues building it. Very much looking forward to using a npm package once it's been accepted. Thanks to you and @charleswong28 for sorting it, much appreciated. |
Thanks @abernix! In fact I don't see the Artifacts tab ... but your direct link works! Seems to be a weird bug of CircleCI.
Will try this way and provide feedback 👍 |
Interesting! Thanks for the note, @aymericbouzy. That is indeed peculiar/buggy, more so since it shows the artifact tree without the tab when I go to that link in Incognito mode. It looks like the tab reliably shows, so long as you're logged in as SOME user in their "New UX" experience: https://app.circleci.com/jobs/github/apollographql/apollo-server/59244/artifacts Looking forward to knowing how your use of it goes! (No hurry.) |
I managed to make it work 🎉 Some feedback on my "developer experience" :
provider:
apiGateway:
binaryMediaTypes:
- "*/*" I thought this was not very well documented, so including it in the documentation will help people get started I believe. What's particularly hard about this bug is that when this property is missing, the file simply shows up as corrupted, but there are no errors per say, no warning, nothing to point you at what is wrong about your setup. It is not defined this way by default, which may be an issue with Serverless Framework? I don't know why it is this way, but at the moment, this is a real footgun.
const streamToBuffer = stream =>
new Promise(async resolve => {
const chunks = []
stream
.on("data", data => chunks.push(data))
.on("end", () => resolve(Buffer.concat(chunks)))
}) I guess I have to open up an issue on https://github.com/jaydenseric/graphql-upload ?
Overall : the code works really well. There are a number of loop holes in the documentation that could easily be fixed I guess. I'm willing to suggest some changes to the documentation through some PRs 👍 I can also create separate issues if needed. |
@aymericbouzy Regarding some of the points that you highlighted, I had made an example repository that talked about the binary media types for API GW. It also has it all working with s3, were you not aware of this example? If you were and it wasn't helpful, please let me know why so I can make it more so. |
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've left some comments throughout. I'm glad that the current implementation is working for some right now, but I think there needs to be more pattern alignment with other packages in this repository and improvements to TypeScript typings to call it ready.
It's really worth noting that, while I certainly appreciate the efforts here, we are actively working on removing individual integrations, like apollo-server-lambda
, apollo-server-express
, etc.) as part of the #3184 proposal in the Apollo Server 3.0 roadmap (#2360 ). Those are to be replaced with a more ubiquitous transport.
That's to say, this work may need to be re-visited when that happens. That's not to say that this work will be fruitless though, as there are learnings that will need to be applied to the new approach too, but I'm hopeful that the patterns and duplication across integrations will be substantially reduced.
The first work of that new transport work is appearing (recently) in #3576, but it's still not quite ready. Feel free to tinker with the idea though if you feel so compelled, as it is functional, but it's currently just an HTTP transport and there's no Lambda handler for it just yet. The expectation is that there will be off-the-npm-shelf npm packages, much like the aws-serverless-express
I linked above, that will bridge the gap and coerce the special event
object into a more generic interface (namely, something like http.IncomingMessage
) which can be used in the same way across the various implementations.
export const NODE_MINOR_VERSION: number = parseInt( | ||
process.versions.node.split('.', 2)[1], | ||
10, | ||
); | ||
|
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 is only for tests and our tests only use the latest major versions of these servers. Let's remove this and the bits below that utilize it. Node.js 8 was also never LTS until 8.9.
// This translates the arguments from the middleware into graphQL options It | ||
// provides typings for the integration specific behavior, ideally this would | ||
// be propagated with a generic to the super class | ||
createGraphQLServerOptions( | ||
event: APIGatewayProxyEvent, | ||
context: LambdaContext, | ||
context: LambdaContext |
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.
context: LambdaContext | |
context: LambdaContext, |
): Promise<GraphQLOptions> { | ||
return super.graphQLServerOptions({ event, context }); | ||
return super.graphQLServerOptions({ event, context}); |
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.
Let's leave out unrelated formatting changes.
return super.graphQLServerOptions({ event, context}); | |
return super.graphQLServerOptions({ event, context }); |
@@ -213,19 +225,29 @@ export class ApolloServer extends ApolloServerBase { | |||
} | |||
} | |||
|
|||
const callbackFilter: APIGatewayProxyCallback = (error, result) => { |
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 see this is a pattern that started in the other PR, but any reason to not just declare the response
before the existing representation of callbackFilter
, reference response.end()
inside of it and leave the arguments passed to the function returned from graphqlLambda
the same? (i.e. (event, context, callbackFilter)
)?
I'm just missing the justification for changing so many lines here.
...requestCorsHeadersObject, | ||
const makeCallbackFilter = (doneFn: Function): APIGatewayProxyCallback => { | ||
return (error, result) => { | ||
doneFn(); // needed to close the response |
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.
Per my above comment, why not just call response.end()
here?
request.headers = event.headers; | ||
request.headers["content-type"] = contentType; | ||
|
||
processFileUploads(request, serverParams.response, serverParams.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.
Hmm, okay, based on the casting, I now think you're trying to simulate the behavior of an http.IncomingMessage
which extends stream.Readable
and events.EventEmitter
.
I understand the need to do that, and I fully expect that's what graphql-upload
would expect, but I think this PR needs to find a more structured and strongly-typed way to do that.
Perhaps it's worth using something like this strongly-typed http-lambda
package (though this seems to be published into an npm namespace I didn't find right away; perhaps https://github.com/awslabs/aws-serverless-express is an alternative) to coerce the event into an http.IncomingMessage
?
To be honest, I'd rather we just actually instantiate an http.IncomingMessage
by invoking constructor (i.e. new
) rather than using a stream.Readable
as if it were the right interface. We could create a subclass of stream.Readable
that implements the necessary properties, rather than just setting random properties (e.g. headers
) onto a stream instance. Does that make sense?
} else { | ||
// standard query/mutation - no files uploaded | ||
runQuery(JSON.parse(event.body)); | ||
} | ||
} else { | ||
// just query string params | ||
runQuery(event.queryStringParameters); | ||
} |
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.
Converting these to else if
's at the same depth would be preferable over the if/if/else/else
setup.
import { | ||
renderPlaygroundPage, | ||
RenderPageOptions as PlaygroundRenderPageOptions, | ||
} from '@apollographql/graphql-playground-html'; | ||
|
||
import { graphqlLambda } from './lambdaApollo'; | ||
import { Headers } from 'apollo-server-env'; | ||
import stream from 'stream' |
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.
Import only the symbols which are necessary, rather than the whole module.
import stream from 'stream' | |
import { Writeable } from 'stream' |
@@ -213,19 +225,29 @@ export class ApolloServer extends ApolloServerBase { | |||
} | |||
} |
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'll make reference to this again in comments below, but is there a reason that most of the logic that's being added in lambdaApollo.ts
(below, in this PR) isn't added here instead (before graphqlLambda
is ever kicked off)?
This is more of what I was thinking with my comment on the other PR (https://github.com/apollographql/apollo-server/pull/1739/files#r288265931) and would make this match the other integrations. This is most important from a maintainability standpoint over the the Apollo Server project as a whole since this divergent pattern makes it difficult to grok what's going on (particularly by starting a new pattern which passes two option objects to graphqlLambda
which already has an options
property for its GraphQLOptions
.
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 believe the reason that everyone (me and the orig PR) has been organizing the code to process the files first, before doing the actual query is because the processFileUploads
function's response is actually needed for the query body when it is a multipart form. We're struggling to follow the same design principles of the other integrations (Express, HAPI, Fastify, etc) because those integrations all have the benefit of actual proper middleware implementations at the server level and that's where those implementations process the uploads. The lambda server/proxy doesn't really have that level of middleware as I understand it. And I'm pretty sure we're not going to get into a complete re-write of that part here. However, in those other integrations, middleware acts much the same way as I understand it, by handling the file upload first and then performing the query.
All that said, I did take another stab at it, moving the logic out to ApolloServer.ts
. The graphqlLambda
function kicks off the chain, and we kinda cheat by passing processEvent
as the first arg so that we can await the file upload processing in lambdaApollo.ts
. Not sure it really changes anything though, as we still wait for the upload to finish before running the query, but that seems to be the nature of the beast. If you'd like to see this WIP, you can look at the work in this branch: https://github.com/abcya/apollo-server/tree/event-rework/packages/apollo-server-lambda/src
Please ignore the any
typings all over the place, was just trying to prove a concept quickly. Would definitely need some work there, just didn't want to put in too much effort without discussion (I'm not convinced myself).
The more time we spend on this, the more I see why the original PR structured it the way he did with the .then()
, and I actually think it's a decent approach given the limitations we're up against.
request: { | ||
url: event.path, | ||
|
||
const runQuery = (query:any) => { |
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.
Would the type of this query
be a HttpQueryRequest
?:
export interface HttpQueryRequest { |
I should add, most of all: I still think this is a valuable addition to the v2 series. If you’re willing to keep working through it, I’m happy to see it through! |
@abernix any feedback on my reply about organization of logic? |
any news there ? |
@abernix any way to help move this forward? |
Aim to provide file-upload parity support — as already supported within the other Apollo Server integration packages — via the third-party `graphql-upload` package. Co-authored-by: charleswong28 <chung.triniti@gmail.com> Co-authored-by: Steve Babigian <steve@noisykid.com> Co-authored-by: Jesse Rosenberger <git@jro.cc> Closes: #1419 Closes: #1703 Supersedes: #1739 ...and therefore... Closes: #1739 Supersedes: #3676 ...and therefore... Closes: #3676
@k00k I followed your comment and follow the steps in your repo but I can't uplod images with my lambda function. I was working with serverless-offline I everything works fine, but I had not success with my function in AWS. Do you know which can be the problem? Thanks in advance! |
If you did either the manual method or used the |
It can be plenty of things did you watch your lambda logs ? Can you provide us more details |
Yes, I had an error connecting with Cloudinary. Now is working perfectly! |
Fixes #1419, #1703 file upload using lambda.
Based on original PR #1739
TODO:
After needing
graphql-upload
support inapollo-server-lambda
, we (me and @smschick) found PR #1739 that was created a while ago and never was updated after @abernix had some comments on the PR. The main thing that @abernix had mentioned was being sure that the file upload process was happening in the right order.This PR takes those comments and some of the work from the original PR and hopefully solves the issue. Thanks to @charleswong28 for his work on the orig PR.
Please let me know if there's anything you'd like to see. Currently all tests are passing which includes single and multiple file uploads.