Skip to content

Commit 6d9cd87

Browse files
committed
feat(graphql-codegen): add localhost DNS resolution for *.localhost endpoints during introspection
This fixes DNS resolution issues on macOS where subdomains like api.localhost don't resolve automatically to 127.0.0.1 (unlike browsers which handle *.localhost). Uses undici Agent with custom DNS lookup to resolve *.localhost to 127.0.0.1 at codegen time only. The generated client code remains browser-compatible.
1 parent 699fbbc commit 6d9cd87

File tree

3 files changed

+78
-14
lines changed

3 files changed

+78
-14
lines changed

graphql/codegen/package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@
4444
"fmt:check": "prettier --check .",
4545
"test": "jest --passWithNoTests",
4646
"test:watch": "jest --watch",
47-
"example:codegen:sdk": "tsx src/cli/index.ts generate --config examples/multi-target.config.ts --reactquery",
48-
"example:codegen:orm": "tsx src/cli/index.ts generate --config examples/multi-target.config.ts --orm",
49-
"example:codegen:sdk:schema": "node dist/cli/index.js generate --schema examples/example.schema.graphql --output examples/output/generated-sdk-schema --reactquery",
50-
"example:codegen:orm:schema": "node dist/cli/index.js generate --schema examples/example.schema.graphql --output examples/output/generated-orm-schema --orm",
47+
"example:codegen:sdk": "tsx src/cli/index.ts generate --config examples/multi-target.config.ts --reactquery",
48+
"example:codegen:orm": "tsx src/cli/index.ts generate --config examples/multi-target.config.ts --orm",
49+
"example:codegen:sdk:schema": "node dist/cli/index.js generate --schema examples/example.schema.graphql --output examples/output/generated-sdk-schema --reactquery",
50+
"example:codegen:orm:schema": "node dist/cli/index.js generate --schema examples/example.schema.graphql --output examples/output/generated-orm-schema --orm",
5151
"example:sdk": "tsx examples/react-hooks-sdk-test.tsx",
5252
"example:orm": "tsx examples/orm-sdk-test.ts",
5353
"example:sdk:typecheck": "tsc --noEmit --jsx react --esModuleInterop --skipLibCheck --moduleResolution node examples/react-hooks-sdk-test.tsx"
@@ -72,7 +72,8 @@
7272
"pg-env": "workspace:^",
7373
"pgsql-client": "workspace:^",
7474
"pgsql-seed": "workspace:^",
75-
"prettier": "^3.7.4"
75+
"prettier": "^3.7.4",
76+
"undici": "^7.19.0"
7677
},
7778
"peerDependencies": {
7879
"@tanstack/react-query": "^5.0.0",

graphql/codegen/src/core/introspect/fetch-schema.ts

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,46 @@
11
/**
22
* Fetch GraphQL schema introspection from an endpoint
33
*/
4+
import dns from 'node:dns';
5+
import { Agent } from 'undici';
46
import { SCHEMA_INTROSPECTION_QUERY } from './schema-query';
57
import type { IntrospectionQueryResponse } from '../../types/introspection';
68

9+
/**
10+
* Check if a hostname is localhost or a localhost subdomain
11+
*/
12+
function isLocalhostHostname(hostname: string): boolean {
13+
return hostname === 'localhost' || hostname.endsWith('.localhost');
14+
}
15+
16+
/**
17+
* Create an undici Agent that resolves *.localhost to 127.0.0.1
18+
* This fixes DNS resolution issues on macOS where subdomains like api.localhost
19+
* don't resolve automatically (unlike browsers which handle *.localhost).
20+
*/
21+
function createLocalhostAgent(): Agent {
22+
return new Agent({
23+
connect: {
24+
lookup(hostname, opts, cb) {
25+
if (isLocalhostHostname(hostname)) {
26+
cb(null, '127.0.0.1', 4);
27+
return;
28+
}
29+
dns.lookup(hostname, opts, cb);
30+
},
31+
},
32+
});
33+
}
34+
35+
let localhostAgent: Agent | null = null;
36+
37+
function getLocalhostAgent(): Agent {
38+
if (!localhostAgent) {
39+
localhostAgent = createLocalhostAgent();
40+
}
41+
return localhostAgent;
42+
}
43+
744
export interface FetchSchemaOptions {
845
/** GraphQL endpoint URL */
946
endpoint: string;
@@ -30,13 +67,22 @@ export async function fetchSchema(
3067
): Promise<FetchSchemaResult> {
3168
const { endpoint, authorization, headers = {}, timeout = 30000 } = options;
3269

70+
// Parse the endpoint URL to check for localhost
71+
const url = new URL(endpoint);
72+
const useLocalhostAgent = isLocalhostHostname(url.hostname);
73+
3374
// Build headers
3475
const requestHeaders: Record<string, string> = {
3576
'Content-Type': 'application/json',
3677
Accept: 'application/json',
3778
...headers,
3879
};
3980

81+
// Set Host header for localhost subdomains to preserve routing
82+
if (useLocalhostAgent && url.hostname !== 'localhost') {
83+
requestHeaders['Host'] = url.hostname;
84+
}
85+
4086
if (authorization) {
4187
requestHeaders['Authorization'] = authorization;
4288
}
@@ -45,16 +91,24 @@ export async function fetchSchema(
4591
const controller = new AbortController();
4692
const timeoutId = setTimeout(() => controller.abort(), timeout);
4793

94+
// Build fetch options
95+
const fetchOptions: RequestInit & { dispatcher?: Agent } = {
96+
method: 'POST',
97+
headers: requestHeaders,
98+
body: JSON.stringify({
99+
query: SCHEMA_INTROSPECTION_QUERY,
100+
variables: {},
101+
}),
102+
signal: controller.signal,
103+
};
104+
105+
// Use custom agent for localhost to fix DNS resolution on macOS
106+
if (useLocalhostAgent) {
107+
fetchOptions.dispatcher = getLocalhostAgent();
108+
}
109+
48110
try {
49-
const response = await fetch(endpoint, {
50-
method: 'POST',
51-
headers: requestHeaders,
52-
body: JSON.stringify({
53-
query: SCHEMA_INTROSPECTION_QUERY,
54-
variables: {},
55-
}),
56-
signal: controller.signal,
57-
});
111+
const response = await fetch(endpoint, fetchOptions);
58112

59113
clearTimeout(timeoutId);
60114

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)