Skip to content

Commit 166102f

Browse files
authoredDec 30, 2022
fix: http2 support when using Node ponyfill (#237)
* attempt test * test http2 * no describe * create fetch api * use node fetch and not * change method and path * handle and translate http2 * end req before client close * changeset * comment
1 parent fba62c4 commit 166102f

File tree

3 files changed

+156
-2
lines changed

3 files changed

+156
-2
lines changed
 

‎.changeset/dry-cougars-check.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@whatwg-node/fetch': patch
3+
---
4+
5+
http2 support when using Node ponyfill

‎packages/fetch/dist/create-node-ponyfill.js

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const http2 = require('http2')
12
const handleFileRequest = require("./handle-file-request");
23
const readableStreamToReadable = require("./readableStreamToReadable");
34

@@ -203,8 +204,32 @@ module.exports = function createNodePonyfill(opts = {}) {
203204
if (/^\/\//.test(requestOrUrl.toString())) {
204205
requestOrUrl = "https:" + requestOrUrl.toString();
205206
}
207+
let method = (options || {}).method;
208+
const headers = {};
209+
if ('headers' in (options || {})) {
210+
let isHttp2 = false;
211+
for (const [key, value] of Object.entries(options.headers)) {
212+
if (key.startsWith(':')) {
213+
// omit http2 headers
214+
isHttp2 = true;
215+
} else {
216+
headers[key] = value
217+
}
218+
}
219+
if (isHttp2) {
220+
// translate http2 if applicable
221+
method = options.headers[http2.constants.HTTP2_HEADER_METHOD];
222+
const scheme = options.headers[http2.constants.HTTP2_HEADER_SCHEME];
223+
const authority = options.headers[http2.constants.HTTP2_HEADER_AUTHORITY];
224+
const path = options.headers[http2.constants.HTTP2_HEADER_PATH];
225+
headers.host = authority;
226+
requestOrUrl = `${scheme}://${authority}${path}`
227+
}
228+
}
206229
const fixedOptions = {
207-
...options
230+
...options,
231+
method,
232+
headers,
208233
};
209234
fixedOptions.headers = new ponyfills.Headers(fixedOptions.headers || {});
210235
fixedOptions.headers.set('Connection', 'keep-alive');

‎packages/server/test/node.spec.ts

+125-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ import { createServerAdapter } from '@whatwg-node/server';
22
import { IncomingMessage, ServerResponse } from 'http';
33
import { createTestServer, TestServer } from './test-server';
44
import { createTestContainer } from './create-test-container';
5-
import { Http2ServerRequest, Http2ServerResponse } from 'http2';
5+
import {
6+
createSecureServer as createHttp2SecureServer,
7+
Http2ServerRequest,
8+
Http2ServerResponse,
9+
connect as connectHttp2,
10+
constants as constantsHttp2,
11+
} from 'http2';
12+
import { AddressInfo } from 'net';
13+
import { createFetch } from '@whatwg-node/fetch';
614

715
describe('Node Specific Cases', () => {
816
let testServer: TestServer;
@@ -113,6 +121,122 @@ describe('Node Specific Cases', () => {
113121
adapter.handle(req, res);
114122
adapter(req, res);
115123
});
124+
125+
it.each([
126+
{
127+
fetchImpl: 'default',
128+
fetchAPI: createFetch({ useNodeFetch: false }),
129+
},
130+
{
131+
fetchImpl: 'node-fetch',
132+
fetchAPI: createFetch({ useNodeFetch: true }),
133+
},
134+
])('should support http2 and respond as expected when using $fetchImpl implementation', async ({ fetchAPI }) => {
135+
const adapter = createServerAdapter(req => {
136+
expect(req.method).toBe('POST');
137+
// TODO: only passes if create-node-ponyfill.js is used
138+
// expect(req.headers.get('host')).toMatch(/^localhost:\d+$/);
139+
// expect(req.url).toMatch(/^https:\/\/localhost:\d+\/hi$/);
140+
return new fetchAPI.Response('Hey there!', { status: 418, headers: { 'x-is-this-http2': 'yes' } });
141+
}, fetchAPI.Request);
142+
143+
const key = `-----BEGIN PRIVATE KEY-----
144+
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDL2k3sKtqBQ9lw
145+
ouLuCewSuTCazFjSdzJKLWmm9d9OLRi9SVPaIaes0ItExHFXwVNSXGUlabTSXxVP
146+
x9cJXDtloBnlN+YKK5f8vcpP7a9hquYDKMhM27kP6e8CIugDfXP4rz52o6Jn2ZEz
147+
JrzpbrF3eDtD4uVfXfZeAgR9jilfFI+L5qu5AjZSWtL/YwqVRus1r3ChXBOgvLy/
148+
MN7NJ1W7fDgyCLge1HvDGPidyrHoVezGEtzWUpGatgR6PNhdtI5M/bf+l4+xAHL6
149+
sYeTpg6iAsrE+K3VkBIFgxye7lzXUIXyeQ6ij3DsBVlT6bY80g1QpcTDBoXCOnkS
150+
GEyFuC33AgMBAAECggEBAK7FA8d1Wg43GGW8EKiaMx4+TVB537DZZnE4C/uLkp6Y
151+
hTxLcKtz7Sh5Rt13OeFNqtzSwBjaTp+Jy2Cx6UdqHrZbE7h0OzH++/hA0wHBunoW
152+
pcqRnWBfhIMDQdloCdhsJxBPVlMqqWM1oYnkLVRIhbfyiYUDMzmW+lDQk/788bVE
153+
BmTVY9qkHYt+6Cu97Wt4mVQZS6CS9oaJn3btuUbT7V3x3q5ER7jRmwRUPwFc4uVv
154+
lEFP/UCc3JeK+rEoZVVcafImetLfzwTszQ18BV5Y4plt+kB7OFW8OVpAgrnK6e3g
155+
+RVsN3FhN6QkgkWhhpQOVCqBTNphxmrOnL3shmEJ7gECgYEA/qGeSA+l3Wh9+aUk
156+
wBo7nsyqJa/61K10uLNe47tJe5ZxB11lT3JCNvNhuJ/BTxiGclRTRCSl/VQOZ3JY
157+
s6TR3i6LtykfypyaQCjMDWXRZpEoBpEdwKP+o/9M03oIRs3eGxTm83iC/GCYooQR
158+
tfHJMLlgQufsq5+uGnU/7QMGDwECgYEAzPLQH/lsM7yROyc7Q877scFnYVLX/U+r
159+
6lQROFWuLM3a3DGafg9+kFziZVK7jQ41z/EywuU//XH7UtrjmVlGRZCSbhFGPokw
160+
gO4q2KaDuFyq1iSRIorj3pjXO+zYZoX7fcbMInlpC+oBpU+S1jyRreGgdhkEXtYq
161+
9bQSUntTtPcCgYEA1T661PSt3tfUsI7aUTtm9N3IHNndQeGmH8ywSh4eMy9Rp25T
162+
Gw7AX07CZyD7fmc2qWbveOEMVjTf/0hm+sOsstreTV1Wb5NpJxRDl3DOxowILj+3
163+
4A43glabm3vWlJ1yRdHifMJPSFcJXQkn3+0GphSJhl6++Rg4cZYCHFbs6wECgYEA
164+
xsAWa1uTvdx5LtdN1uVsGqbHHY+cXFAeFOGvzWTxwwti2jTUcLmP8GnTN5Vywkjs
165+
kJqEspJlauBVbLVPENCNoDqidlEUQOMEAZR2QqHAjVJ4bbEKemgcsSqhV8DI3yvB
166+
huj539jDsUUekXTInjAgynJLDRwXq+yfvqUBO7HTrGMCgYBmPS4dcLoBC04MiSIQ
167+
mDYzpcI51XzlyJPQQKjHjck5H4WDV80EIX22krvFMh44IOyqZu3Ou+iSPCR1hi1z
168+
Mp0YOF+YKJ9PCrJu4W/xt1pnxfXe9bTg5HKtN6DmYlSz78EMelSVemaqOgNoIBqC
169+
t6Ra3NuebwL/VQ1JpBhh4eJYZg==
170+
-----END PRIVATE KEY-----`;
171+
const cert = `-----BEGIN CERTIFICATE-----
172+
MIICpDCCAYwCCQClE698xX22XDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
173+
b2NhbGhvc3QwHhcNMjIxMjI4MTc0NDMxWhcNMjMwMTI3MTc0NDMxWjAUMRIwEAYD
174+
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDL
175+
2k3sKtqBQ9lwouLuCewSuTCazFjSdzJKLWmm9d9OLRi9SVPaIaes0ItExHFXwVNS
176+
XGUlabTSXxVPx9cJXDtloBnlN+YKK5f8vcpP7a9hquYDKMhM27kP6e8CIugDfXP4
177+
rz52o6Jn2ZEzJrzpbrF3eDtD4uVfXfZeAgR9jilfFI+L5qu5AjZSWtL/YwqVRus1
178+
r3ChXBOgvLy/MN7NJ1W7fDgyCLge1HvDGPidyrHoVezGEtzWUpGatgR6PNhdtI5M
179+
/bf+l4+xAHL6sYeTpg6iAsrE+K3VkBIFgxye7lzXUIXyeQ6ij3DsBVlT6bY80g1Q
180+
pcTDBoXCOnkSGEyFuC33AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAL3BU99gtkpT
181+
9KwkFtn18+j3OFaJzoj+WPrC0YvbPx3KqnZeEH3MvqyRk7WMcUVrPnmLY9S7oPYb
182+
AYpkSwuvh0374zVcAn0CYRWSafj6nM9xmEWk3F28jfF+XemS1F8/Z0NyLJSVytIb
183+
bdEO2Po5v+S/RlE/QE7ONaKYecOPMTcW7FeEze77DOJXTvkuM5ab/Wj1mbSE40sH
184+
VhEJmi7pGnPZOobUh3QhhpvqJ4myRCyrHKS53l1RJJ+7/XXVq6WDHAcMxHseRKnb
185+
ziIZM/48ENV+m5yXVvUZJaKOggThi+RhLSwIyVzn8ScawkXS70bZtI4CrSTXu3H9
186+
/huiHkWkMUs=
187+
-----END CERTIFICATE-----`;
188+
189+
const server = createHttp2SecureServer({ key, cert }, adapter);
190+
server.listen(0);
191+
const port = (server.address() as AddressInfo).port;
192+
193+
// Node's fetch API does not support HTTP/2, we use the http2 module directly instead
194+
195+
const client = connectHttp2(`https://localhost:${port}`, { ca: cert });
196+
197+
const req = client.request({
198+
[constantsHttp2.HTTP2_HEADER_METHOD]: 'POST',
199+
[constantsHttp2.HTTP2_HEADER_PATH]: '/hi',
200+
});
201+
202+
await expect(
203+
new Promise((resolve, reject) => {
204+
req.on(
205+
'response',
206+
({
207+
date, // omit date from snapshot
208+
...headers
209+
}) => {
210+
let data = '';
211+
req.on('data', chunk => {
212+
data += chunk;
213+
});
214+
req.on('end', () => {
215+
resolve({
216+
headers,
217+
data,
218+
});
219+
});
220+
}
221+
);
222+
req.on('error', reject);
223+
})
224+
).resolves.toMatchInlineSnapshot(`
225+
{
226+
"data": "Hey there!",
227+
"headers": {
228+
":status": 418,
229+
"content-type": "text/plain;charset=UTF-8",
230+
"x-is-this-http2": "yes",
231+
Symbol(nodejs.http2.sensitiveHeaders): [],
232+
},
233+
}
234+
`);
235+
236+
req.end();
237+
client.close();
238+
server.close();
239+
});
116240
});
117241

118242
function sleep(ms: number) {

0 commit comments

Comments
 (0)
Please sign in to comment.