Skip to content

Commit 611b275

Browse files
authored
fix(refactor): async await instead of Promise chain (#711)
* fail early, fail fast * improve * improve tests * AbortSignal should result in a status code 0 * minor change * streamline further * remove case * status 500 for abort error * add Dispatcher testcase for timeout * adapt requested changes
1 parent bc059c8 commit 611b275

6 files changed

+267
-184
lines changed

package-lock.json

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"prettier": "3.3.2",
4242
"semantic-release-plugin-update-version-in-files": "^1.0.0",
4343
"typescript": "^5.0.0",
44+
"undici": "^6.19.2",
4445
"vitest": "^2.0.0"
4546
},
4647
"release": {

src/fetch-wrapper.ts

+116-124
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,10 @@
11
import { isPlainObject } from "./is-plain-object.js";
22
import { RequestError } from "@octokit/request-error";
3-
import type { EndpointInterface } from "@octokit/types";
3+
import type { EndpointInterface, OctokitResponse } from "@octokit/types";
44

5-
export default function fetchWrapper(
5+
export default async function fetchWrapper(
66
requestOptions: ReturnType<EndpointInterface>,
7-
) {
8-
const log =
9-
requestOptions.request && requestOptions.request.log
10-
? requestOptions.request.log
11-
: console;
12-
const parseSuccessResponseBody =
13-
requestOptions.request?.parseSuccessResponseBody !== false;
14-
15-
if (
16-
isPlainObject(requestOptions.body) ||
17-
Array.isArray(requestOptions.body)
18-
) {
19-
requestOptions.body = JSON.stringify(requestOptions.body);
20-
}
21-
22-
let headers: { [header: string]: string } = {};
23-
let status: number;
24-
let url: string;
25-
7+
): Promise<OctokitResponse<any>> {
268
const fetch: typeof globalThis.fetch =
279
requestOptions.request?.fetch || globalThis.fetch;
2810

@@ -32,109 +14,46 @@ export default function fetchWrapper(
3214
);
3315
}
3416

35-
return fetch(requestOptions.url, {
36-
method: requestOptions.method,
37-
body: requestOptions.body,
38-
redirect: requestOptions.request?.redirect,
39-
// Header values must be `string`
40-
headers: Object.fromEntries(
41-
Object.entries(requestOptions.headers).map(([name, value]) => [
42-
name,
43-
String(value),
44-
]),
45-
),
46-
signal: requestOptions.request?.signal,
47-
// duplex must be set if request.body is ReadableStream or Async Iterables.
48-
// See https://fetch.spec.whatwg.org/#dom-requestinit-duplex.
49-
...(requestOptions.body && { duplex: "half" }),
50-
})
51-
.then(async (response) => {
52-
url = response.url;
53-
status = response.status;
54-
55-
for (const keyAndValue of response.headers) {
56-
headers[keyAndValue[0]] = keyAndValue[1];
57-
}
58-
59-
if ("deprecation" in headers) {
60-
const matches =
61-
headers.link && headers.link.match(/<([^>]+)>; rel="deprecation"/);
62-
const deprecationLink = matches && matches.pop();
63-
log.warn(
64-
`[@octokit/request] "${requestOptions.method} ${
65-
requestOptions.url
66-
}" is deprecated. It is scheduled to be removed on ${headers.sunset}${
67-
deprecationLink ? `. See ${deprecationLink}` : ""
68-
}`,
69-
);
70-
}
71-
72-
if (status === 204 || status === 205) {
73-
return;
74-
}
75-
76-
// GitHub API returns 200 for HEAD requests
77-
if (requestOptions.method === "HEAD") {
78-
if (status < 400) {
79-
return;
80-
}
81-
82-
throw new RequestError(response.statusText, status, {
83-
response: {
84-
url,
85-
status,
86-
headers,
87-
data: undefined,
88-
},
89-
request: requestOptions,
90-
});
91-
}
92-
93-
if (status === 304) {
94-
throw new RequestError("Not modified", status, {
95-
response: {
96-
url,
97-
status,
98-
headers,
99-
data: await getResponseData(response),
100-
},
101-
request: requestOptions,
102-
});
103-
}
104-
105-
if (status >= 400) {
106-
const data = await getResponseData(response);
107-
108-
const error = new RequestError(toErrorMessage(data), status, {
109-
response: {
110-
url,
111-
status,
112-
headers,
113-
data,
114-
},
115-
request: requestOptions,
116-
});
17+
const log = requestOptions.request?.log || console;
18+
const parseSuccessResponseBody =
19+
requestOptions.request?.parseSuccessResponseBody !== false;
11720

21+
const body =
22+
isPlainObject(requestOptions.body) || Array.isArray(requestOptions.body)
23+
? JSON.stringify(requestOptions.body)
24+
: requestOptions.body;
25+
26+
// Header values must be `string`
27+
const requestHeaders = Object.fromEntries(
28+
Object.entries(requestOptions.headers).map(([name, value]) => [
29+
name,
30+
String(value),
31+
]),
32+
);
33+
34+
let fetchResponse: Response;
35+
36+
try {
37+
fetchResponse = await fetch(requestOptions.url, {
38+
method: requestOptions.method,
39+
body,
40+
redirect: requestOptions.request?.redirect,
41+
headers: requestHeaders,
42+
signal: requestOptions.request?.signal,
43+
// duplex must be set if request.body is ReadableStream or Async Iterables.
44+
// See https://fetch.spec.whatwg.org/#dom-requestinit-duplex.
45+
...(requestOptions.body && { duplex: "half" }),
46+
});
47+
// wrap fetch errors as RequestError if it is not a AbortError
48+
} catch (error) {
49+
let message = "Unknown Error";
50+
if (error instanceof Error) {
51+
if (error.name === "AbortError") {
52+
(error as RequestError).status = 500;
11853
throw error;
11954
}
12055

121-
return parseSuccessResponseBody
122-
? await getResponseData(response)
123-
: response.body;
124-
})
125-
.then((data) => {
126-
return {
127-
status,
128-
url,
129-
headers,
130-
data,
131-
};
132-
})
133-
.catch((error) => {
134-
if (error instanceof RequestError) throw error;
135-
else if (error.name === "AbortError") throw error;
136-
137-
let message = error.message;
56+
message = error.message;
13857

13958
// undici throws a TypeError for network errors
14059
// and puts the error message in `error.cause`
@@ -146,14 +65,87 @@ export default function fetchWrapper(
14665
message = error.cause;
14766
}
14867
}
68+
}
69+
70+
const requestError = new RequestError(message, 500, {
71+
request: requestOptions,
72+
});
73+
requestError.cause = error;
74+
75+
throw requestError;
76+
}
77+
78+
const status = fetchResponse.status;
79+
const url = fetchResponse.url;
80+
const responseHeaders: { [header: string]: string } = {};
81+
82+
for (const [key, value] of fetchResponse.headers) {
83+
responseHeaders[key] = value;
84+
}
85+
86+
const octokitResponse: OctokitResponse<any> = {
87+
url,
88+
status,
89+
headers: responseHeaders,
90+
data: "",
91+
};
92+
93+
if ("deprecation" in responseHeaders) {
94+
const matches =
95+
responseHeaders.link &&
96+
responseHeaders.link.match(/<([^>]+)>; rel="deprecation"/);
97+
const deprecationLink = matches && matches.pop();
98+
log.warn(
99+
`[@octokit/request] "${requestOptions.method} ${
100+
requestOptions.url
101+
}" is deprecated. It is scheduled to be removed on ${responseHeaders.sunset}${
102+
deprecationLink ? `. See ${deprecationLink}` : ""
103+
}`,
104+
);
105+
}
106+
107+
if (status === 204 || status === 205) {
108+
return octokitResponse;
109+
}
110+
111+
// GitHub API returns 200 for HEAD requests
112+
if (requestOptions.method === "HEAD") {
113+
if (status < 400) {
114+
return octokitResponse;
115+
}
116+
117+
throw new RequestError(fetchResponse.statusText, status, {
118+
response: octokitResponse,
119+
request: requestOptions,
120+
});
121+
}
122+
123+
if (status === 304) {
124+
octokitResponse.data = await getResponseData(fetchResponse);
125+
126+
throw new RequestError("Not modified", status, {
127+
response: octokitResponse,
128+
request: requestOptions,
129+
});
130+
}
131+
132+
if (status >= 400) {
133+
octokitResponse.data = await getResponseData(fetchResponse);
149134

150-
throw new RequestError(message, 500, {
151-
request: requestOptions,
152-
});
135+
throw new RequestError(toErrorMessage(octokitResponse.data), status, {
136+
response: octokitResponse,
137+
request: requestOptions,
153138
});
139+
}
140+
141+
octokitResponse.data = parseSuccessResponseBody
142+
? await getResponseData(fetchResponse)
143+
: fetchResponse.body;
144+
145+
return octokitResponse;
154146
}
155147

156-
async function getResponseData(response: Response) {
148+
async function getResponseData(response: Response): Promise<any> {
157149
const contentType = response.headers.get("content-type");
158150
if (/application\/json/.test(contentType!)) {
159151
return (

test/mock-request-http-server.ts

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import defaults from "../src/defaults.ts";
1010

1111
export default async function mockRequestHttpServer(
1212
requestListener: RequestListener,
13+
fetch = globalThis.fetch,
1314
): Promise<
1415
RequestInterface<object> & {
1516
closeMockServer: () => void;
@@ -25,6 +26,9 @@ export default async function mockRequestHttpServer(
2526
const request = withDefaults(endpoint, {
2627
...defaults,
2728
baseUrl,
29+
request: {
30+
fetch,
31+
},
2832
}) as RequestInterface<object> & {
2933
closeMockServer: () => void;
3034
baseUrlMockServer: string;

0 commit comments

Comments
 (0)