From 8d2be646a73e74654f5e0ff1c3fdf71a64d1648a Mon Sep 17 00:00:00 2001 From: Tobias Lampert Date: Fri, 28 Nov 2025 14:39:20 +0100 Subject: [PATCH] Allow headers type in DefaultContext to be Record or HttpHeaders --- .changeset/floppy-states-throw.md | 5 + .../http/src/http-batch-link.ts | 13 +- packages/apollo-angular/http/src/http-link.ts | 17 +- packages/apollo-angular/http/src/types.ts | 10 +- packages/apollo-angular/http/src/utils.ts | 20 +- .../http/tests/http-batch-link.spec.ts | 247 +++++++++--------- .../http/tests/http-link.spec.ts | 177 +++++++------ 7 files changed, 262 insertions(+), 227 deletions(-) create mode 100644 .changeset/floppy-states-throw.md diff --git a/.changeset/floppy-states-throw.md b/.changeset/floppy-states-throw.md new file mode 100644 index 000000000..61284fac9 --- /dev/null +++ b/.changeset/floppy-states-throw.md @@ -0,0 +1,5 @@ +--- +'apollo-angular': patch +--- + +Allow headers type in DefaultContext to be Record or HttpHeaders diff --git a/packages/apollo-angular/http/src/http-batch-link.ts b/packages/apollo-angular/http/src/http-batch-link.ts index 1400399a6..0501e8e1d 100644 --- a/packages/apollo-angular/http/src/http-batch-link.ts +++ b/packages/apollo-angular/http/src/http-batch-link.ts @@ -7,6 +7,8 @@ import { BatchLink } from '@apollo/client/link/batch'; import type { HttpLink } from './http-link'; import { Body, Context, OperationPrinter, Request } from './types'; import { + convertHeadersToArray, + convertToHttpHeaders, createHeadersWithClientAwareness, fetch, mergeHeaders, @@ -38,7 +40,7 @@ export const defaults = { */ export function pick>( context: Context, - options: HttpBatchLink.Options, + options: Omit, key: K, ): ReturnType> { return prioritize(context[key], options[key], defaults[key]); @@ -161,7 +163,9 @@ export class HttpBatchLinkHandler extends ApolloLink { return operations.reduce( (headers: HttpHeaders, operation: ApolloLink.Operation) => { const { headers: contextHeaders } = operation.getContext(); - return contextHeaders ? mergeHeaders(headers, contextHeaders) : headers; + return contextHeaders + ? mergeHeaders(headers, convertToHttpHeaders(contextHeaders)) + : headers; }, createHeadersWithClientAwareness({ headers: this.options.headers, @@ -187,8 +191,7 @@ export class HttpBatchLinkHandler extends ApolloLink { return Math.random().toString(36).substring(2, 11); } - const headers = - context.headers && context.headers.keys().map((k: string) => context.headers!.get(k)); + const headers = convertHeadersToArray(context.headers); const opts = JSON.stringify({ includeQuery: context.includeQuery, @@ -199,7 +202,7 @@ export class HttpBatchLinkHandler extends ApolloLink { return prioritize(context.uri, this.options.uri, '') + opts; } - public request( + public override request( op: ApolloLink.Operation, forward: ApolloLink.ForwardFunction, ): Observable { diff --git a/packages/apollo-angular/http/src/http-link.ts b/packages/apollo-angular/http/src/http-link.ts index e4bf84a2b..e69f864b5 100644 --- a/packages/apollo-angular/http/src/http-link.ts +++ b/packages/apollo-angular/http/src/http-link.ts @@ -9,14 +9,14 @@ import { Context, ExtractFiles, FetchOptions, - HttpRequestOptions, OperationPrinter, Request, + RequestOptions, } from './types'; import { createHeadersWithClientAwareness, fetch, mergeHeaders, mergeHttpContext } from './utils'; export declare namespace HttpLink { - export interface Options extends FetchOptions, HttpRequestOptions { + export interface Options extends FetchOptions, RequestOptions { operationPrinter?: OperationPrinter; useGETForQueries?: boolean; extractFiles?: ExtractFiles; @@ -62,6 +62,11 @@ export class HttpLinkHandler extends ApolloLink { method = 'GET'; } + const headers = mergeHeaders( + this.options.headers, + createHeadersWithClientAwareness(context), + ); + const req: Request = { method, url: typeof url === 'function' ? url(operation) : url, @@ -72,7 +77,7 @@ export class HttpLinkHandler extends ApolloLink { options: { withCredentials, useMultipart, - headers: this.options.headers, + headers, context: httpContext, }, }; @@ -85,10 +90,6 @@ export class HttpLinkHandler extends ApolloLink { (req.body as Body).query = this.print(operation.query); } - const headers = createHeadersWithClientAwareness(context); - - req.options.headers = mergeHeaders(req.options.headers, headers); - const sub = fetch(req, this.httpClient, this.options.extractFiles).subscribe({ next: response => { operation.setContext({ response }); @@ -106,7 +107,7 @@ export class HttpLinkHandler extends ApolloLink { }); } - public request(op: ApolloLink.Operation): Observable { + public override request(op: ApolloLink.Operation): Observable { return this.requester(op); } } diff --git a/packages/apollo-angular/http/src/types.ts b/packages/apollo-angular/http/src/types.ts index e55b2582e..8466d460f 100644 --- a/packages/apollo-angular/http/src/types.ts +++ b/packages/apollo-angular/http/src/types.ts @@ -7,14 +7,14 @@ declare module '@apollo/client' { } export type HttpRequestOptions = { - headers?: HttpHeaders; + headers?: HttpHeaders | Record; withCredentials?: boolean; useMultipart?: boolean; httpContext?: HttpContext; }; -export type RequestOptions = Omit & { - context?: HttpContext; +export type RequestOptions = Omit & { + headers?: HttpHeaders; }; export type URIFunction = (operation: ApolloLink.Operation) => string; @@ -37,11 +37,13 @@ export type Body = { export interface Context extends FetchOptions, HttpRequestOptions {} +type HttpClientRequestOptions = Omit & { context: HttpContext }; + export type Request = { method: string; url: string; body: Body | Body[]; - options: RequestOptions; + options: HttpClientRequestOptions; }; export type ExtractedFiles = { diff --git a/packages/apollo-angular/http/src/utils.ts b/packages/apollo-angular/http/src/utils.ts index 47d93117c..29d369904 100644 --- a/packages/apollo-angular/http/src/utils.ts +++ b/packages/apollo-angular/http/src/utils.ts @@ -106,16 +106,25 @@ export const fetch = ( }); }; +export const convertToHttpHeaders = ( + headers: HttpHeaders | Record | undefined, +): HttpHeaders => (headers instanceof HttpHeaders ? headers : new HttpHeaders(headers)); + +export const convertHeadersToArray = ( + headers: HttpHeaders | Record | undefined, +): string[] => + headers instanceof HttpHeaders + ? headers.keys().map((k: string) => headers.get(k)!) + : Object.values(headers ?? {}); + export const mergeHeaders = ( source: HttpHeaders | undefined, destination: HttpHeaders, ): HttpHeaders => { if (source && destination) { - const merged = destination + return destination .keys() .reduce((headers, name) => headers.set(name, destination.getAll(name)!), source); - - return merged; } return destination || source; @@ -146,10 +155,7 @@ export function createHeadersWithClientAwareness(context: Record) { // `clientAwareness` object is found in the context. These headers are // set first, followed by the rest of the headers pulled from // `context.headers`. - let headers = - context.headers && context.headers instanceof HttpHeaders - ? context.headers - : new HttpHeaders(context.headers); + let headers = convertToHttpHeaders(context.headers); if (context.clientAwareness) { const { name, version } = context.clientAwareness; diff --git a/packages/apollo-angular/http/tests/http-batch-link.spec.ts b/packages/apollo-angular/http/tests/http-batch-link.spec.ts index 5cdd3af93..022c9ca8c 100644 --- a/packages/apollo-angular/http/tests/http-batch-link.spec.ts +++ b/packages/apollo-angular/http/tests/http-batch-link.spec.ts @@ -285,141 +285,150 @@ describe('HttpBatchLink', () => { }, 50); })); - test('should support headers from constructor options', () => - new Promise(done => { - const link = httpLink.create({ - uri: 'graphql', - headers: new HttpHeaders().set('X-Custom-Header', 'foo'), - }); - const op = { - query: gql` - query heroes { - heroes { - name + describe.each([ + ['Record', true], + ['HttpHeaders', false], + ])('Headers as %s', (_, useRecord) => { + const createHeaders = (name: string, value: string): HttpHeaders | Record => { + return useRecord ? { [name]: value } : new HttpHeaders().set(name, value); + }; + + test('should support headers from constructor options', () => + new Promise(done => { + const link = httpLink.create({ + uri: 'graphql', + headers: new HttpHeaders().set('X-Custom-Header', 'foo'), + }); + const op = { + query: gql` + query heroes { + heroes { + name + } } - } - `, - }; + `, + }; - execute(link, op).subscribe(noop); + execute(link, op).subscribe(noop); - setTimeout(() => { - httpBackend.match(req => { - expect(req.headers.get('X-Custom-Header')).toEqual('foo'); - done(); - return true; - }); - }, 50); - })); + setTimeout(() => { + httpBackend.match(req => { + expect(req.headers.get('X-Custom-Header')).toEqual('foo'); + done(); + return true; + }); + }, 50); + })); - test('should support headers from context', () => - new Promise(done => { - const link = httpLink.create({ - uri: 'graphql', - }); - const op = { - query: gql` - query heroes { - heroes { - name + test('should support headers from context', () => + new Promise(done => { + const link = httpLink.create({ + uri: 'graphql', + }); + const op = { + query: gql` + query heroes { + heroes { + name + } } - } - `, - context: { - headers: new HttpHeaders().set('X-Custom-Header', 'foo'), - }, - }; + `, + context: { + headers: createHeaders('X-Custom-Header', 'foo'), + }, + }; - execute(link, op).subscribe(noop); + execute(link, op).subscribe(noop); - setTimeout(() => { - httpBackend.match(req => { - expect(req.headers.get('X-Custom-Header')).toEqual('foo'); - done(); - return true; - }); - }, 50); - })); + setTimeout(() => { + httpBackend.match(req => { + expect(req.headers.get('X-Custom-Header')).toEqual('foo'); + done(); + return true; + }); + }, 50); + })); - test('should support headers from context', () => - new Promise(done => { - const link = httpLink.create({ - uri: 'graphql', - }); - const clientAwareness = { - name: 'iOS', - version: '1.0.0', - }; - const op = { - query: gql` - query heroes { - heroes { - name + test('should support headers from context', () => + new Promise(done => { + const link = httpLink.create({ + uri: 'graphql', + }); + const clientAwareness = { + name: 'iOS', + version: '1.0.0', + }; + const op = { + query: gql` + query heroes { + heroes { + name + } } - } - `, - context: { - clientAwareness, - }, - }; + `, + context: { + clientAwareness, + }, + }; - execute(link, op).subscribe(noop); + execute(link, op).subscribe(noop); - setTimeout(() => { - httpBackend.match(req => { - expect(req.headers.get('apollographql-client-name')).toBe(clientAwareness.name); - expect(req.headers.get('apollographql-client-version')).toBe(clientAwareness.version); - done(); - return true; - }); - }, 50); - })); + setTimeout(() => { + httpBackend.match(req => { + expect(req.headers.get('apollographql-client-name')).toBe(clientAwareness.name); + expect(req.headers.get('apollographql-client-version')).toBe(clientAwareness.version); + done(); + return true; + }); + }, 50); + })); - test('should merge headers from context and constructor options', () => - new Promise(done => { - const link = httpLink.create({ - uri: 'graphql', - headers: new HttpHeaders().set('X-Custom-Foo', 'foo'), - batchKey: () => 'bachKey', - }); - const op1 = { - query: gql` - query heroes { - heroes { - name + test('should merge headers from context and constructor options', () => + new Promise(done => { + const link = httpLink.create({ + uri: 'graphql', + headers: new HttpHeaders().set('X-Custom-Foo', 'foo'), + batchKey: () => 'bachKey', + }); + const op1 = { + query: gql` + query heroes { + heroes { + name + } } - } - `, - context: { - headers: new HttpHeaders().set('X-Custom-Bar', 'bar'), - }, - }; - const op2 = { - query: gql` - query heroes { - heroes { - name + `, + context: { + headers: createHeaders('X-Custom-Bar', 'bar'), + }, + }; + const op2 = { + query: gql` + query heroes { + heroes { + name + } } - } - `, - context: { - headers: new HttpHeaders().set('X-Custom-Baz', 'baz'), - }, - }; + `, + context: { + headers: createHeaders('X-Custom-Baz', 'baz'), + }, + }; - execute(link, op1).subscribe(noop); - execute(link, op2).subscribe(noop); + execute(link, op1).subscribe(noop); + execute(link, op2).subscribe(noop); - setTimeout(() => { - httpBackend.match(req => { - expect(req.headers.get('X-Custom-Foo')).toEqual('foo'); - expect(req.headers.get('X-Custom-Bar')).toEqual('bar'); - expect(req.headers.get('X-Custom-Baz')).toEqual('baz'); - done(); - return true; - }); - }, 50); - })); + setTimeout(() => { + httpBackend.match(req => { + expect(req.headers.get('X-Custom-Foo')).toEqual('foo'); + expect(req.headers.get('X-Custom-Bar')).toEqual('bar'); + expect(req.headers.get('X-Custom-Baz')).toEqual('baz'); + done(); + return true; + }); + }, 50); + })); + }); test('should support dynamic uri based on context.uri', () => new Promise(done => { diff --git a/packages/apollo-angular/http/tests/http-link.spec.ts b/packages/apollo-angular/http/tests/http-link.spec.ts index aa778ab10..e271bb288 100644 --- a/packages/apollo-angular/http/tests/http-link.spec.ts +++ b/packages/apollo-angular/http/tests/http-link.spec.ts @@ -306,108 +306,117 @@ describe('HttpLink', () => { }); }); - test('should support headers from constructor options', () => { - const link = httpLink.create({ - uri: 'graphql', - headers: new HttpHeaders().set('X-Custom-Header', 'foo'), - }); - const op = { - query: gql` - query heroes { - heroes { - name - } - } - `, + describe.each([ + ['Record', true], + ['HttpHeaders', false], + ])('Headers as %s', (_, useRecord) => { + const createHeaders = (name: string, value: string): HttpHeaders | Record => { + return useRecord ? { [name]: value } : new HttpHeaders().set(name, value); }; - execute(link, op).subscribe(noop); + test('should support headers from constructor options', () => { + const link = httpLink.create({ + uri: 'graphql', + headers: new HttpHeaders().set('X-Custom-Header', 'foo'), + }); + const op = { + query: gql` + query heroes { + heroes { + name + } + } + `, + }; - httpBackend.match(req => { - expect(req.headers.get('X-Custom-Header')).toBe('foo'); - return true; - }); - }); + execute(link, op).subscribe(noop); - test('should support headers from context', () => { - const link = httpLink.create({ - uri: 'graphql', + httpBackend.match(req => { + expect(req.headers.get('X-Custom-Header')).toBe('foo'); + return true; + }); }); - const op = { - query: gql` - query heroes { - heroes { - name + + test('should support headers from context', () => { + const link = httpLink.create({ + uri: 'graphql', + }); + const op = { + query: gql` + query heroes { + heroes { + name + } } - } - `, - context: { - headers: new HttpHeaders().set('X-Custom-Header', 'foo'), - }, - }; + `, + context: { + headers: createHeaders('X-Custom-Header', 'foo'), + }, + }; - execute(link, op).subscribe(noop); + execute(link, op).subscribe(noop); - httpBackend.match(req => { - expect(req.headers.get('X-Custom-Header')).toBe('foo'); - return true; + httpBackend.match(req => { + expect(req.headers.get('X-Custom-Header')).toBe('foo'); + return true; + }); }); - }); - test('should use clientAwareness from context in headers', () => { - const link = httpLink.create({ - uri: 'graphql', - }); - const clientAwareness = { - name: 'iOS', - version: '1.0.0', - }; - const op = { - query: gql` - query heroes { - heroes { - name + test('should use clientAwareness from context in headers', () => { + const link = httpLink.create({ + uri: 'graphql', + }); + const clientAwareness = { + name: 'iOS', + version: '1.0.0', + }; + const op = { + query: gql` + query heroes { + heroes { + name + } } - } - `, - context: { - clientAwareness, - }, - }; + `, + context: { + clientAwareness, + }, + }; - execute(link, op).subscribe(noop); + execute(link, op).subscribe(noop); - httpBackend.match(req => { - expect(req.headers.get('apollographql-client-name')).toBe(clientAwareness.name); - expect(req.headers.get('apollographql-client-version')).toBe(clientAwareness.version); - return true; + httpBackend.match(req => { + expect(req.headers.get('apollographql-client-name')).toBe(clientAwareness.name); + expect(req.headers.get('apollographql-client-version')).toBe(clientAwareness.version); + return true; + }); }); - }); - test('should merge headers from context and constructor options', () => { - const link = httpLink.create({ - uri: 'graphql', - headers: new HttpHeaders().set('X-Custom-Foo', 'foo'), - }); - const op = { - query: gql` - query heroes { - heroes { - name + test('should merge headers from context and constructor options', () => { + const link = httpLink.create({ + uri: 'graphql', + headers: new HttpHeaders().set('X-Custom-Foo', 'foo'), + }); + const op = { + query: gql` + query heroes { + heroes { + name + } } - } - `, - context: { - headers: new HttpHeaders().set('X-Custom-Bar', 'bar'), - }, - }; + `, + context: { + headers: createHeaders('X-Custom-Bar', 'bar'), + }, + }; - execute(link, op).subscribe(noop); + execute(link, op).subscribe(noop); - httpBackend.match(req => { - expect(req.headers.get('X-Custom-Foo')).toBe('foo'); - expect(req.headers.get('X-Custom-Bar')).toBe('bar'); - return true; + httpBackend.match(req => { + expect(req.headers.get('X-Custom-Foo')).toBe('foo'); + expect(req.headers.get('X-Custom-Bar')).toBe('bar'); + return true; + }); }); });