Skip to content

Commit

Permalink
fix: http2 support when using Node ponyfill (#237)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
enisdenjo committed Dec 30, 2022
1 parent fba62c4 commit 166102f
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/dry-cougars-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@whatwg-node/fetch': patch
---

http2 support when using Node ponyfill
27 changes: 26 additions & 1 deletion packages/fetch/dist/create-node-ponyfill.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const http2 = require('http2')
const handleFileRequest = require("./handle-file-request");
const readableStreamToReadable = require("./readableStreamToReadable");

Expand Down Expand Up @@ -203,8 +204,32 @@ module.exports = function createNodePonyfill(opts = {}) {
if (/^\/\//.test(requestOrUrl.toString())) {
requestOrUrl = "https:" + requestOrUrl.toString();
}
let method = (options || {}).method;
const headers = {};
if ('headers' in (options || {})) {
let isHttp2 = false;
for (const [key, value] of Object.entries(options.headers)) {
if (key.startsWith(':')) {
// omit http2 headers
isHttp2 = true;
} else {
headers[key] = value
}
}
if (isHttp2) {
// translate http2 if applicable
method = options.headers[http2.constants.HTTP2_HEADER_METHOD];
const scheme = options.headers[http2.constants.HTTP2_HEADER_SCHEME];
const authority = options.headers[http2.constants.HTTP2_HEADER_AUTHORITY];
const path = options.headers[http2.constants.HTTP2_HEADER_PATH];
headers.host = authority;
requestOrUrl = `${scheme}://${authority}${path}`
}
}
const fixedOptions = {
...options
...options,
method,
headers,
};
fixedOptions.headers = new ponyfills.Headers(fixedOptions.headers || {});
fixedOptions.headers.set('Connection', 'keep-alive');
Expand Down
126 changes: 125 additions & 1 deletion packages/server/test/node.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import { createServerAdapter } from '@whatwg-node/server';
import { IncomingMessage, ServerResponse } from 'http';
import { createTestServer, TestServer } from './test-server';
import { createTestContainer } from './create-test-container';
import { Http2ServerRequest, Http2ServerResponse } from 'http2';
import {
createSecureServer as createHttp2SecureServer,
Http2ServerRequest,
Http2ServerResponse,
connect as connectHttp2,
constants as constantsHttp2,
} from 'http2';
import { AddressInfo } from 'net';
import { createFetch } from '@whatwg-node/fetch';

describe('Node Specific Cases', () => {
let testServer: TestServer;
Expand Down Expand Up @@ -113,6 +121,122 @@ describe('Node Specific Cases', () => {
adapter.handle(req, res);
adapter(req, res);
});

it.each([
{
fetchImpl: 'default',
fetchAPI: createFetch({ useNodeFetch: false }),
},
{
fetchImpl: 'node-fetch',
fetchAPI: createFetch({ useNodeFetch: true }),
},
])('should support http2 and respond as expected when using $fetchImpl implementation', async ({ fetchAPI }) => {
const adapter = createServerAdapter(req => {
expect(req.method).toBe('POST');
// TODO: only passes if create-node-ponyfill.js is used
// expect(req.headers.get('host')).toMatch(/^localhost:\d+$/);
// expect(req.url).toMatch(/^https:\/\/localhost:\d+\/hi$/);
return new fetchAPI.Response('Hey there!', { status: 418, headers: { 'x-is-this-http2': 'yes' } });
}, fetchAPI.Request);

const key = `-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDL2k3sKtqBQ9lw
ouLuCewSuTCazFjSdzJKLWmm9d9OLRi9SVPaIaes0ItExHFXwVNSXGUlabTSXxVP
x9cJXDtloBnlN+YKK5f8vcpP7a9hquYDKMhM27kP6e8CIugDfXP4rz52o6Jn2ZEz
JrzpbrF3eDtD4uVfXfZeAgR9jilfFI+L5qu5AjZSWtL/YwqVRus1r3ChXBOgvLy/
MN7NJ1W7fDgyCLge1HvDGPidyrHoVezGEtzWUpGatgR6PNhdtI5M/bf+l4+xAHL6
sYeTpg6iAsrE+K3VkBIFgxye7lzXUIXyeQ6ij3DsBVlT6bY80g1QpcTDBoXCOnkS
GEyFuC33AgMBAAECggEBAK7FA8d1Wg43GGW8EKiaMx4+TVB537DZZnE4C/uLkp6Y
hTxLcKtz7Sh5Rt13OeFNqtzSwBjaTp+Jy2Cx6UdqHrZbE7h0OzH++/hA0wHBunoW
pcqRnWBfhIMDQdloCdhsJxBPVlMqqWM1oYnkLVRIhbfyiYUDMzmW+lDQk/788bVE
BmTVY9qkHYt+6Cu97Wt4mVQZS6CS9oaJn3btuUbT7V3x3q5ER7jRmwRUPwFc4uVv
lEFP/UCc3JeK+rEoZVVcafImetLfzwTszQ18BV5Y4plt+kB7OFW8OVpAgrnK6e3g
+RVsN3FhN6QkgkWhhpQOVCqBTNphxmrOnL3shmEJ7gECgYEA/qGeSA+l3Wh9+aUk
wBo7nsyqJa/61K10uLNe47tJe5ZxB11lT3JCNvNhuJ/BTxiGclRTRCSl/VQOZ3JY
s6TR3i6LtykfypyaQCjMDWXRZpEoBpEdwKP+o/9M03oIRs3eGxTm83iC/GCYooQR
tfHJMLlgQufsq5+uGnU/7QMGDwECgYEAzPLQH/lsM7yROyc7Q877scFnYVLX/U+r
6lQROFWuLM3a3DGafg9+kFziZVK7jQ41z/EywuU//XH7UtrjmVlGRZCSbhFGPokw
gO4q2KaDuFyq1iSRIorj3pjXO+zYZoX7fcbMInlpC+oBpU+S1jyRreGgdhkEXtYq
9bQSUntTtPcCgYEA1T661PSt3tfUsI7aUTtm9N3IHNndQeGmH8ywSh4eMy9Rp25T
Gw7AX07CZyD7fmc2qWbveOEMVjTf/0hm+sOsstreTV1Wb5NpJxRDl3DOxowILj+3
4A43glabm3vWlJ1yRdHifMJPSFcJXQkn3+0GphSJhl6++Rg4cZYCHFbs6wECgYEA
xsAWa1uTvdx5LtdN1uVsGqbHHY+cXFAeFOGvzWTxwwti2jTUcLmP8GnTN5Vywkjs
kJqEspJlauBVbLVPENCNoDqidlEUQOMEAZR2QqHAjVJ4bbEKemgcsSqhV8DI3yvB
huj539jDsUUekXTInjAgynJLDRwXq+yfvqUBO7HTrGMCgYBmPS4dcLoBC04MiSIQ
mDYzpcI51XzlyJPQQKjHjck5H4WDV80EIX22krvFMh44IOyqZu3Ou+iSPCR1hi1z
Mp0YOF+YKJ9PCrJu4W/xt1pnxfXe9bTg5HKtN6DmYlSz78EMelSVemaqOgNoIBqC
t6Ra3NuebwL/VQ1JpBhh4eJYZg==
-----END PRIVATE KEY-----`;
const cert = `-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQClE698xX22XDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjIxMjI4MTc0NDMxWhcNMjMwMTI3MTc0NDMxWjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDL
2k3sKtqBQ9lwouLuCewSuTCazFjSdzJKLWmm9d9OLRi9SVPaIaes0ItExHFXwVNS
XGUlabTSXxVPx9cJXDtloBnlN+YKK5f8vcpP7a9hquYDKMhM27kP6e8CIugDfXP4
rz52o6Jn2ZEzJrzpbrF3eDtD4uVfXfZeAgR9jilfFI+L5qu5AjZSWtL/YwqVRus1
r3ChXBOgvLy/MN7NJ1W7fDgyCLge1HvDGPidyrHoVezGEtzWUpGatgR6PNhdtI5M
/bf+l4+xAHL6sYeTpg6iAsrE+K3VkBIFgxye7lzXUIXyeQ6ij3DsBVlT6bY80g1Q
pcTDBoXCOnkSGEyFuC33AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAL3BU99gtkpT
9KwkFtn18+j3OFaJzoj+WPrC0YvbPx3KqnZeEH3MvqyRk7WMcUVrPnmLY9S7oPYb
AYpkSwuvh0374zVcAn0CYRWSafj6nM9xmEWk3F28jfF+XemS1F8/Z0NyLJSVytIb
bdEO2Po5v+S/RlE/QE7ONaKYecOPMTcW7FeEze77DOJXTvkuM5ab/Wj1mbSE40sH
VhEJmi7pGnPZOobUh3QhhpvqJ4myRCyrHKS53l1RJJ+7/XXVq6WDHAcMxHseRKnb
ziIZM/48ENV+m5yXVvUZJaKOggThi+RhLSwIyVzn8ScawkXS70bZtI4CrSTXu3H9
/huiHkWkMUs=
-----END CERTIFICATE-----`;

const server = createHttp2SecureServer({ key, cert }, adapter);
server.listen(0);
const port = (server.address() as AddressInfo).port;

// Node's fetch API does not support HTTP/2, we use the http2 module directly instead

const client = connectHttp2(`https://localhost:${port}`, { ca: cert });

const req = client.request({
[constantsHttp2.HTTP2_HEADER_METHOD]: 'POST',
[constantsHttp2.HTTP2_HEADER_PATH]: '/hi',
});

await expect(
new Promise((resolve, reject) => {
req.on(
'response',
({
date, // omit date from snapshot
...headers
}) => {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
resolve({
headers,
data,
});
});
}
);
req.on('error', reject);
})
).resolves.toMatchInlineSnapshot(`
{
"data": "Hey there!",
"headers": {
":status": 418,
"content-type": "text/plain;charset=UTF-8",
"x-is-this-http2": "yes",
Symbol(nodejs.http2.sensitiveHeaders): [],
},
}
`);

req.end();
client.close();
server.close();
});
});

function sleep(ms: number) {
Expand Down

0 comments on commit 166102f

Please sign in to comment.