Skip to content

Commit 5753b8d

Browse files
committed
refactor: use @constructive-io/fetch package instead of inline wrapper
Replace the inlined localhostFetch wrapper with an import from @constructive-io/fetch (dev-utils#81). The package handles: - *.localhost DNS rewriting via node:http (fixes ENOTFOUND) - Host header preservation (fixes silent drop by undici) - Automatic passthrough for non-localhost URLs and browsers This moves the fetch logic to a shared library so it can be iterated on in one place rather than being baked into every generated client file. Added @constructive-io/fetch ^0.1.0 as a dependency to all SDK packages (constructive-sdk, constructive-react, constructive-cli, migrate-client).
1 parent 5142f53 commit 5753b8d

20 files changed

Lines changed: 144 additions & 680 deletions

File tree

graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ exports[`client-generator generateOrmClientFile generates OrmClient class with e
104104
* @generated by @constructive-io/graphql-codegen
105105
* DO NOT EDIT - changes will be overwritten
106106
*/
107+
import { createFetch } from '@constructive-io/fetch';
107108
import type {
108109
GraphQLAdapter,
109110
GraphQLError,
@@ -116,49 +117,13 @@ export type {
116117
QueryResult,
117118
} from '@constructive-io/graphql-query/runtime';
118119
119-
/**
120-
* Rewrite *.localhost URLs so they resolve in Node.js.
121-
*
122-
* Node's undici-backed fetch cannot resolve *.localhost subdomains
123-
* (ENOTFOUND) and silently drops the Host header (forbidden by spec).
124-
* This wrapper rewrites the URL to plain localhost and injects the
125-
* original Host header as a best-effort (works with node:http-based
126-
* fetch implementations; Node's built-in fetch may still drop it).
127-
*
128-
* In browsers *.localhost resolves natively, so this is a no-op.
129-
*/
130-
function localhostFetch(
131-
input: RequestInfo | URL,
132-
init?: RequestInit,
133-
): Promise<Response> {
134-
const url = new URL(
135-
typeof input === 'string'
136-
? input
137-
: input instanceof URL
138-
? input.href
139-
: input.url,
140-
);
141-
142-
if (url.hostname.endsWith('.localhost') && url.hostname !== 'localhost') {
143-
const originalHost = url.host;
144-
url.hostname = 'localhost';
145-
const headers = new Headers(init?.headers);
146-
if (!headers.has('host')) {
147-
headers.set('host', originalHost);
148-
}
149-
return fetch(url.toString(), { ...init, headers });
150-
}
151-
152-
return fetch(input, init);
153-
}
154-
155120
/**
156121
* Default adapter that uses fetch for HTTP requests.
157122
*
158-
* When no custom fetch is provided, uses localhostFetch which rewrites
159-
* *.localhost URLs to plain localhost for Node.js DNS compatibility.
160-
* Pass a custom fetch to override (e.g. for Node.js Host header
161-
* preservation via node:http, or for test mocking).
123+
* When no custom fetch is provided, uses @constructive-io/fetch which
124+
* handles *.localhost DNS rewriting and Host header preservation in
125+
* Node.js. Pass a custom fetch to override for test mocking or custom
126+
* proxy/credentials.
162127
*/
163128
export class FetchAdapter implements GraphQLAdapter {
164129
private headers: Record<string, string>;
@@ -170,7 +135,7 @@ export class FetchAdapter implements GraphQLAdapter {
170135
fetchFn?: typeof globalThis.fetch,
171136
) {
172137
this.headers = headers ?? {};
173-
this.fetchFn = fetchFn ?? localhostFetch;
138+
this.fetchFn = fetchFn ?? createFetch();
174139
}
175140
176141
async execute<T>(
@@ -240,10 +205,9 @@ export interface OrmClientConfig {
240205
/** Default headers for HTTP requests (only used with endpoint) */
241206
headers?: Record<string, string>;
242207
/**
243-
* Custom fetch implementation. Defaults to a wrapper that rewrites
244-
* *.localhost URLs for Node.js compatibility. Pass your own fetch
245-
* for full Node.js Host header support (e.g. via node:http), test
246-
* mocking, or custom proxy/credentials.
208+
* Custom fetch implementation. Defaults to @constructive-io/fetch
209+
* which handles *.localhost DNS and Host headers in Node.js.
210+
* Pass your own for test mocking or custom proxy/credentials.
247211
*/
248212
fetch?: typeof globalThis.fetch;
249213
/** Custom adapter for GraphQL execution (overrides endpoint/headers/fetch) */

graphql/codegen/src/__tests__/codegen/client-generator.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ describe('client-generator', () => {
6868
expect(result.content).toContain('config.fetch');
6969
});
7070

71-
it('includes localhostFetch wrapper for *.localhost DNS rewriting', () => {
71+
it('imports createFetch from @constructive-io/fetch', () => {
7272
const result = generateOrmClientFile();
7373

74-
expect(result.content).toContain('function localhostFetch');
75-
expect(result.content).toContain(".endsWith('.localhost')");
76-
expect(result.content).toContain("url.hostname = 'localhost'");
77-
expect(result.content).toContain("headers.set('host', originalHost)");
74+
expect(result.content).toContain(
75+
"import { createFetch } from '@constructive-io/fetch'",
76+
);
77+
expect(result.content).toContain('createFetch()');
7878
});
7979
});
8080

graphql/codegen/src/core/codegen/templates/orm-client.ts

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* Any changes here will affect all generated ORM clients.
99
*/
1010

11+
import { createFetch } from '@constructive-io/fetch';
1112
import type {
1213
GraphQLAdapter,
1314
GraphQLError,
@@ -20,49 +21,13 @@ export type {
2021
QueryResult,
2122
} from '@constructive-io/graphql-query/runtime';
2223

23-
/**
24-
* Rewrite *.localhost URLs so they resolve in Node.js.
25-
*
26-
* Node's undici-backed fetch cannot resolve *.localhost subdomains
27-
* (ENOTFOUND) and silently drops the Host header (forbidden by spec).
28-
* This wrapper rewrites the URL to plain localhost and injects the
29-
* original Host header as a best-effort (works with node:http-based
30-
* fetch implementations; Node's built-in fetch may still drop it).
31-
*
32-
* In browsers *.localhost resolves natively, so this is a no-op.
33-
*/
34-
function localhostFetch(
35-
input: RequestInfo | URL,
36-
init?: RequestInit,
37-
): Promise<Response> {
38-
const url = new URL(
39-
typeof input === 'string'
40-
? input
41-
: input instanceof URL
42-
? input.href
43-
: input.url,
44-
);
45-
46-
if (url.hostname.endsWith('.localhost') && url.hostname !== 'localhost') {
47-
const originalHost = url.host;
48-
url.hostname = 'localhost';
49-
const headers = new Headers(init?.headers);
50-
if (!headers.has('host')) {
51-
headers.set('host', originalHost);
52-
}
53-
return fetch(url.toString(), { ...init, headers });
54-
}
55-
56-
return fetch(input, init);
57-
}
58-
5924
/**
6025
* Default adapter that uses fetch for HTTP requests.
6126
*
62-
* When no custom fetch is provided, uses localhostFetch which rewrites
63-
* *.localhost URLs to plain localhost for Node.js DNS compatibility.
64-
* Pass a custom fetch to override (e.g. for Node.js Host header
65-
* preservation via node:http, or for test mocking).
27+
* When no custom fetch is provided, uses @constructive-io/fetch which
28+
* handles *.localhost DNS rewriting and Host header preservation in
29+
* Node.js. Pass a custom fetch to override for test mocking or custom
30+
* proxy/credentials.
6631
*/
6732
export class FetchAdapter implements GraphQLAdapter {
6833
private headers: Record<string, string>;
@@ -74,7 +39,7 @@ export class FetchAdapter implements GraphQLAdapter {
7439
fetchFn?: typeof globalThis.fetch,
7540
) {
7641
this.headers = headers ?? {};
77-
this.fetchFn = fetchFn ?? localhostFetch;
42+
this.fetchFn = fetchFn ?? createFetch();
7843
}
7944

8045
async execute<T>(
@@ -144,10 +109,9 @@ export interface OrmClientConfig {
144109
/** Default headers for HTTP requests (only used with endpoint) */
145110
headers?: Record<string, string>;
146111
/**
147-
* Custom fetch implementation. Defaults to a wrapper that rewrites
148-
* *.localhost URLs for Node.js compatibility. Pass your own fetch
149-
* for full Node.js Host header support (e.g. via node:http), test
150-
* mocking, or custom proxy/credentials.
112+
* Custom fetch implementation. Defaults to @constructive-io/fetch
113+
* which handles *.localhost DNS and Host headers in Node.js.
114+
* Pass your own for test mocking or custom proxy/credentials.
151115
*/
152116
fetch?: typeof globalThis.fetch;
153117
/** Custom adapter for GraphQL execution (overrides endpoint/headers/fetch) */

sdk/constructive-cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
],
4747
"dependencies": {
4848
"@0no-co/graphql.web": "^1.1.2",
49+
"@constructive-io/fetch": "^0.1.0",
4950
"@constructive-io/graphql-query": "workspace:^",
5051
"@constructive-io/graphql-types": "workspace:^",
5152
"appstash": "^0.7.0",

sdk/constructive-cli/src/admin/orm/client.ts

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* @generated by @constructive-io/graphql-codegen
44
* DO NOT EDIT - changes will be overwritten
55
*/
6+
import { createFetch } from '@constructive-io/fetch';
67
import type {
78
GraphQLAdapter,
89
GraphQLError,
@@ -15,49 +16,13 @@ export type {
1516
QueryResult,
1617
} from '@constructive-io/graphql-query/runtime';
1718

18-
/**
19-
* Rewrite *.localhost URLs so they resolve in Node.js.
20-
*
21-
* Node's undici-backed fetch cannot resolve *.localhost subdomains
22-
* (ENOTFOUND) and silently drops the Host header (forbidden by spec).
23-
* This wrapper rewrites the URL to plain localhost and injects the
24-
* original Host header as a best-effort (works with node:http-based
25-
* fetch implementations; Node's built-in fetch may still drop it).
26-
*
27-
* In browsers *.localhost resolves natively, so this is a no-op.
28-
*/
29-
function localhostFetch(
30-
input: RequestInfo | URL,
31-
init?: RequestInit,
32-
): Promise<Response> {
33-
const url = new URL(
34-
typeof input === 'string'
35-
? input
36-
: input instanceof URL
37-
? input.href
38-
: input.url,
39-
);
40-
41-
if (url.hostname.endsWith('.localhost') && url.hostname !== 'localhost') {
42-
const originalHost = url.host;
43-
url.hostname = 'localhost';
44-
const headers = new Headers(init?.headers);
45-
if (!headers.has('host')) {
46-
headers.set('host', originalHost);
47-
}
48-
return fetch(url.toString(), { ...init, headers });
49-
}
50-
51-
return fetch(input, init);
52-
}
53-
5419
/**
5520
* Default adapter that uses fetch for HTTP requests.
5621
*
57-
* When no custom fetch is provided, uses localhostFetch which rewrites
58-
* *.localhost URLs to plain localhost for Node.js DNS compatibility.
59-
* Pass a custom fetch to override (e.g. for Node.js Host header
60-
* preservation via node:http, or for test mocking).
22+
* When no custom fetch is provided, uses @constructive-io/fetch which
23+
* handles *.localhost DNS rewriting and Host header preservation in
24+
* Node.js. Pass a custom fetch to override for test mocking or custom
25+
* proxy/credentials.
6126
*/
6227
export class FetchAdapter implements GraphQLAdapter {
6328
private headers: Record<string, string>;
@@ -69,7 +34,7 @@ export class FetchAdapter implements GraphQLAdapter {
6934
fetchFn?: typeof globalThis.fetch,
7035
) {
7136
this.headers = headers ?? {};
72-
this.fetchFn = fetchFn ?? localhostFetch;
37+
this.fetchFn = fetchFn ?? createFetch();
7338
}
7439

7540
async execute<T>(
@@ -139,10 +104,9 @@ export interface OrmClientConfig {
139104
/** Default headers for HTTP requests (only used with endpoint) */
140105
headers?: Record<string, string>;
141106
/**
142-
* Custom fetch implementation. Defaults to a wrapper that rewrites
143-
* *.localhost URLs for Node.js compatibility. Pass your own fetch
144-
* for full Node.js Host header support (e.g. via node:http), test
145-
* mocking, or custom proxy/credentials.
107+
* Custom fetch implementation. Defaults to @constructive-io/fetch
108+
* which handles *.localhost DNS and Host headers in Node.js.
109+
* Pass your own for test mocking or custom proxy/credentials.
146110
*/
147111
fetch?: typeof globalThis.fetch;
148112
/** Custom adapter for GraphQL execution (overrides endpoint/headers/fetch) */

sdk/constructive-cli/src/auth/orm/client.ts

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* @generated by @constructive-io/graphql-codegen
44
* DO NOT EDIT - changes will be overwritten
55
*/
6+
import { createFetch } from '@constructive-io/fetch';
67
import type {
78
GraphQLAdapter,
89
GraphQLError,
@@ -15,49 +16,13 @@ export type {
1516
QueryResult,
1617
} from '@constructive-io/graphql-query/runtime';
1718

18-
/**
19-
* Rewrite *.localhost URLs so they resolve in Node.js.
20-
*
21-
* Node's undici-backed fetch cannot resolve *.localhost subdomains
22-
* (ENOTFOUND) and silently drops the Host header (forbidden by spec).
23-
* This wrapper rewrites the URL to plain localhost and injects the
24-
* original Host header as a best-effort (works with node:http-based
25-
* fetch implementations; Node's built-in fetch may still drop it).
26-
*
27-
* In browsers *.localhost resolves natively, so this is a no-op.
28-
*/
29-
function localhostFetch(
30-
input: RequestInfo | URL,
31-
init?: RequestInit,
32-
): Promise<Response> {
33-
const url = new URL(
34-
typeof input === 'string'
35-
? input
36-
: input instanceof URL
37-
? input.href
38-
: input.url,
39-
);
40-
41-
if (url.hostname.endsWith('.localhost') && url.hostname !== 'localhost') {
42-
const originalHost = url.host;
43-
url.hostname = 'localhost';
44-
const headers = new Headers(init?.headers);
45-
if (!headers.has('host')) {
46-
headers.set('host', originalHost);
47-
}
48-
return fetch(url.toString(), { ...init, headers });
49-
}
50-
51-
return fetch(input, init);
52-
}
53-
5419
/**
5520
* Default adapter that uses fetch for HTTP requests.
5621
*
57-
* When no custom fetch is provided, uses localhostFetch which rewrites
58-
* *.localhost URLs to plain localhost for Node.js DNS compatibility.
59-
* Pass a custom fetch to override (e.g. for Node.js Host header
60-
* preservation via node:http, or for test mocking).
22+
* When no custom fetch is provided, uses @constructive-io/fetch which
23+
* handles *.localhost DNS rewriting and Host header preservation in
24+
* Node.js. Pass a custom fetch to override for test mocking or custom
25+
* proxy/credentials.
6126
*/
6227
export class FetchAdapter implements GraphQLAdapter {
6328
private headers: Record<string, string>;
@@ -69,7 +34,7 @@ export class FetchAdapter implements GraphQLAdapter {
6934
fetchFn?: typeof globalThis.fetch,
7035
) {
7136
this.headers = headers ?? {};
72-
this.fetchFn = fetchFn ?? localhostFetch;
37+
this.fetchFn = fetchFn ?? createFetch();
7338
}
7439

7540
async execute<T>(
@@ -139,10 +104,9 @@ export interface OrmClientConfig {
139104
/** Default headers for HTTP requests (only used with endpoint) */
140105
headers?: Record<string, string>;
141106
/**
142-
* Custom fetch implementation. Defaults to a wrapper that rewrites
143-
* *.localhost URLs for Node.js compatibility. Pass your own fetch
144-
* for full Node.js Host header support (e.g. via node:http), test
145-
* mocking, or custom proxy/credentials.
107+
* Custom fetch implementation. Defaults to @constructive-io/fetch
108+
* which handles *.localhost DNS and Host headers in Node.js.
109+
* Pass your own for test mocking or custom proxy/credentials.
146110
*/
147111
fetch?: typeof globalThis.fetch;
148112
/** Custom adapter for GraphQL execution (overrides endpoint/headers/fetch) */

0 commit comments

Comments
 (0)