Skip to content

feat(client): specify headers for individual subscribe calls#110

Closed
RMHonor wants to merge 1 commit intoenisdenjo:masterfrom
RMHonor:master
Closed

feat(client): specify headers for individual subscribe calls#110
RMHonor wants to merge 1 commit intoenisdenjo:masterfrom
RMHonor:master

Conversation

@RMHonor
Copy link

@RMHonor RMHonor commented Nov 21, 2024

This allows each subscribe call on the client to specify request headers, useful if your requests need to send different headers depending on the operation.

It will merge the headers with any specified on the client, overriding any duplicates.

Note: this only applies to distinct connection mode, as single connection won't be able to send new headers per oepration as there's only one request.

Example, pulling the Apollo context headers to match the HttpLink behaviour:

export class SSELink extends ApolloLink {
  private client: Client;

  constructor(options: ClientOptions) {
    super();
    this.client = createClient(options);
  }

  public request(operation: Operation): Observable<FetchResult> {
    return new Observable((sink) => {
      return this.client.subscribe<FetchResult>(
        { ...operation, query: print(operation.query) },
        {
          next: sink.next.bind(sink),
          complete: sink.complete.bind(sink),
          error: sink.error.bind(sink),
        },
        undefined,
        operation.getContext().headers,
      );
    });
  }
}

@RMHonor RMHonor marked this pull request as ready for review November 21, 2024 10:40
Copy link
Owner

@enisdenjo enisdenjo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! Certnaily something we could improve.

By convention, I do things the other way around - instead of adding an extra argument to the subscribe and thus fixing the 3rd argument to always be headers (where you could potentially want to change other things, like the url or retry behaviour or any other related config option); I'd rather add an extra argument to headers instead which accepts the RequestParams and changes headers accordingly.

Not only would you remove the lock on the subscribe method; but, since there's a high chance multiple subscribes will need to change headers, you would also centralise the headers manipulation (instead of changing multiple subscribes you would change one headers).

What do you think?

@RMHonor
Copy link
Author

RMHonor commented Nov 26, 2024

That was my first instinct as well, but the RequestParams type purely contains GQL information, and by exposing this on the subscribe call, it allows any sort of manipulation at the point of request. In my example case it allows us access to the Apollo client context so we can match the HttpLink behvaiour.

However I recognise that we probably shouldn't be designing an API around alternative systems or specific use cases.

An alternative idea, could the requestParams take arbitrary data (a bit like extensions but not sent on the network), which would then be accessible in the headers callback? I think this follows patterns I've seen elsewhere.

// extend the request params type
interface RequestParams<T = any> {
  operationName?: string;
  query: string;
  variables?: Record<string, unknown>;
  extensions?: Record<string, unknown>;
  extra?: T;
}

// extra accessible from headers
const client = createClient({
  uri: "https://theguild.com/graphql",
  headers: (requestParams) => ({
    "x-my-header": requestParams.extra,
  }),
});

// call subscribe with arbitrary params
client.subscribe<ExecutionResult>(
  { ...operation, query: print(operation.query), extra: "foo" },
  {
   next: sink.next.bind(sink),
   complete: sink.complete.bind(sink),
   error: sink.error.bind(sink),
 },
  undefined,
  operation.getContext().headers,
);

@CarsonF
Copy link
Contributor

CarsonF commented Oct 21, 2025

I personally do not like an extra prop, it feels hacky and has to be extracted before the JSON is serialized. What about a second argument?

A generic context argument, that could be anything.

type MyContext = ApolloOperation;
const client = new Client<false, MyContext>({
  headers: operation => operation.getContext().http.headers,
});

client.subscribe({/* unchanged */}, operation);

This keeps the request body pure GQL and allows forwarding extra request-based/operational data, that could be anything, to the configuration options.

@CarsonF
Copy link
Contributor

CarsonF commented Oct 21, 2025

Ah I'm seeing now there are multiple arguments already.
Specifically the optional event listener arg.
I should've finished my coffee ☕️

Perhaps another is still ok?

@CarsonF
Copy link
Contributor

CarsonF commented Oct 21, 2025

I updated the Apollo recipe in #124 to include a workaround to allow this.

enisdenjo pushed a commit that referenced this pull request Oct 22, 2025
enisdenjo pushed a commit that referenced this pull request Oct 22, 2025
# [2.6.0](v2.5.4...v2.6.0) (2025-10-22)

### Features

* **client:** Support dynamic URLs and headers per request for distinct connection mode ([#124](#124)) ([7be7251](7be7251)), closes [#110](#110) [#113](#113)
@enisdenjo
Copy link
Owner

🎉 This issue has been resolved in version 2.6.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@enisdenjo enisdenjo added the released Has been released and published label Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released Has been released and published

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments