profusion / apollo-federation-file-upload

Add file upload support to Apollo Federated services.
32 stars 27 forks source link

Ability to add request headers to GQL request on file upload #61

Open dominik-myszkowski opened 1 year ago

dominik-myszkowski commented 1 year ago

🚀 Feature Request

Description

Implementation details

Potential caveats

Acceptance criteria

Additional context and visual reference

*

superlevure commented 1 year ago

I'm having the same issue, I can't figure out why the headers set in willSendRequest are ignored. Any clue?

superlevure commented 1 year ago

Actually, this was fixed by https://github.com/profusion/apollo-federation-file-upload/commit/962583d88afa8843a0e0d4cce71696417d8aec50 but not released yet ?

BobbiSixkiller commented 1 year ago

Same problem here. I can see that the gateway sets the headers based on the context via the willSendRequest method but the headers don't propagate to the corresponding service with subgraph. Any solutions pls? Right now I can't authenticate user accessing upload resolver implemeted by the microservice :/

BobbiSixkiller commented 1 year ago

@superlevure please have you somehow managed to get it to set the headers? If so could you please provide a workaround?

BobbiSixkiller commented 1 year ago

For anyone having this issue, my workaround for now is simply constructing the headers object based on context value received via GraphQLDataSourceProcessOptions args. Somehow the request?.http?.headers returns undefined so I relied on the context, where my authenticated user is set. So basically I tweaked the processFiles method like this

private async processFiles(
    args: GraphQLDataSourceProcessOptions,
    fileVariables: FileVariablesTuple[]
): ProcessResult {
    const { context, request } = args;
    const form = new FormData();

    const variables = cloneDeep(request.variables || {});
    fileVariables.forEach(([variableName]: FileVariablesTuple): void => {
        set(variables, variableName, null);
    });

    const operations = JSON.stringify({
        query: request.query,
        variables,
    });

    form.append("operations", operations);

    const fileMap: { [key: string]: string[] } = {};

    const resolvedFiles: FileUpload[] = await Promise.all(
        fileVariables.map(
            async (
                [variableName, file]: FileVariablesTuple,
                i: number
            ): Promise<FileUpload> => {
                const fileUpload: FileUpload = await file;
                fileMap[i] = [`variables.${variableName}`];
                return fileUpload;
            }
        )
    );

    // This must come before the file contents append bellow
    form.append("map", JSON.stringify(fileMap));
    await this.addDataHandler(form, resolvedFiles);

    // This must happen before constructing the request headers
    // otherwise any custom headers set in willSendRequest are ignored
    if (this.willSendRequest) {
        await this.willSendRequest(args);
    }

    console.log(context.user, context.locale);

    const headers = {
        // ...Object.fromEntries(request?.http?.headers || []),
        ...Object.fromEntries([["user", JSON.stringify(context[user])]]),
        ...form.getHeaders(),
    };

    console.log(headers);

    Object.assign(headers, form.getHeaders() || {});

    const httpRequest = {
        headers,
        method: "POST",
        url: this.url,
    };

    const options = {
        ...httpRequest,
        body: form,
    };

    // NOTE: there is currently a type mismatch related to Headers in @apollo/gateway and apollo-server-env:
    //
    //  >> you should ensure that you pass "plain" objects rather than Headers or Request objects,
    //  >> as the newer version has slightly different logic about how to recognize Headers and
    //  >> Request objects.
    //
    // see:
    // - https://github.com/apollographql/federation/pull/1906
    // - https://github.com/profusion/apollo-federation-file-upload/issues/52#issuecomment-1148946002
    request.http = httpRequest as Exclude<typeof request.http, undefined>;

    let httpResponse: Response | undefined;

    try {
        httpResponse = await this.fetcher(this.url, options);

        const body = await this.parseBody(httpResponse);

        if (!isObject(body)) {
            throw new Error(`Expected JSON response body, but received: ${body}`);
        }
        const response = {
            ...body,
            http: httpResponse,
        };

        if (typeof this.didReceiveResponse === "function") {
            return this.didReceiveResponse({ context, request, response });
        }

        return response;
    } catch (error) {
        this.didEncounterError(error, options, httpResponse);
        throw error;
    }
}`
wabrit commented 1 year ago

Ran into this problem too; it seems like the code here is the culprit as it never passes the headers down to the Request constructor. This will cause lots of issues with subgraphs that need access to e.g. Authorization or apollo-require-preflight headers to function correctly.

As mentioned above there seems to be a commit that fixes this - can this be released soon?

           request.http = {
                headers,
                method: 'POST',
                url: this.url,
            };
            if (this.willSendRequest) {
                yield this.willSendRequest(args);
            }
            const options = Object.assign(Object.assign({}, request.http), { 
                // Apollo types are not up-to-date, make TS happy
                body: form });
            const httpRequest = new apollo_server_env_1.Request(request.http.url, options);
paulbijancoch commented 1 year ago

will this be released soon? 💯

g7fernandes commented 1 year ago

https://medium.com/profusion-engineering/file-uploads-graphql-and-apollo-federation-c5a878707f4c

KMoscIszko commented 1 year ago

Any idea, I got stucked on this. Upload works fine, yet headers are not overwritten.