popsicle-transport-http
Version:
Popsicle transport for sending requests over HTTP1 and HTTP2
532 lines • 23.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = require("path");
const fs_1 = require("fs");
const node_1 = require("servie/dist/node");
const util_1 = require("util");
const index_1 = require("./index");
const http_1 = require("./test-server/http");
const http2_1 = require("./test-server/http2");
const ca = fs_1.readFileSync(path_1.join(__dirname, "./test-server/support/ca-crt.pem"));
const wait = util_1.promisify(setTimeout);
describe("popsicle transport http", () => {
const httpAddress = http_1.server.listen(0).address();
const httpsAddress = http_1.tlsServer.listen(0).address();
const http2Address = http2_1.server.listen(0).address();
const http2TlsAddress = http2_1.tlsServer.listen(0).address();
const TEST_HTTP_URL = `http://localhost:${httpAddress.port}`;
const TEST_HTTPS_URL = `https://localhost:${httpsAddress.port}`;
const TEST_HTTP2_URL = `http://localhost:${http2Address.port}`;
const TEST_HTTP2_TLS_URL = `https://localhost:${http2TlsAddress.port}`;
const ALL_URLS = [
TEST_HTTP_URL,
TEST_HTTPS_URL,
TEST_HTTP2_URL,
TEST_HTTP2_TLS_URL,
];
afterAll(() => {
http_1.server.close();
http_1.tlsServer.close();
http2_1.server.close();
http2_1.tlsServer.close();
});
const done = () => {
throw new TypeError("Invalid request");
};
it("should return 2xx statuses", async () => {
const res = await index_1.transport()(new node_1.Request(`${TEST_HTTP_URL}/status/204`), done);
expect(res.ok).toEqual(true);
expect(res.status).toEqual(204);
expect(res.statusText).toEqual("No Content");
});
it("should return 4xx statuses", async () => {
const res = await index_1.transport()(new node_1.Request(`${TEST_HTTP_URL}/status/404`), done);
expect(res.ok).toEqual(false);
expect(res.status).toEqual(404);
expect(res.statusText).toEqual("Not Found");
});
it("should return 5xx statuses", async () => {
const res = await index_1.transport()(new node_1.Request(`${TEST_HTTP_URL}/status/500`), done);
expect(res.ok).toEqual(false);
expect(res.status).toEqual(500);
expect(res.statusText).toEqual("Internal Server Error");
});
it("should send path without query", async () => {
const req = new node_1.Request(`${TEST_HTTP_URL}/url`);
const res = await index_1.transport()(req, done);
expect(res.status).toEqual(200);
expect(await res.text()).toEqual("/url");
});
it("should send query params", async () => {
const req = new node_1.Request(`${TEST_HTTP_URL}/url?test=true`);
const res = await index_1.transport()(req, done);
expect(res.status).toEqual(200);
expect(await res.text()).toEqual("/url?test=true");
});
it("should send post data", async () => {
const req = new node_1.Request(`${TEST_HTTP_URL}/echo`, {
method: "POST",
body: "example data",
headers: {
"content-type": "application/octet-stream",
},
});
const res = await index_1.transport()(req, done);
expect(res.status).toEqual(200);
expect(res.statusText).toEqual("OK");
expect(res.headers.get("Content-Type")).toEqual("application/octet-stream");
expect(await res.text()).toEqual("example data");
});
it("should send arraybuffer", async () => {
const buffer = new ArrayBuffer(3);
const body = new Uint8Array(buffer);
body.set([1, 2, 3]);
const req = new node_1.Request(`${TEST_HTTP_URL}/echo`, {
method: "POST",
body: buffer,
});
const res = await index_1.transport()(req, done);
expect(res.status).toEqual(200);
expect(res.statusText).toEqual("OK");
expect(res.headers.get("Content-Type")).toEqual("application/octet-stream");
expect(await res.arrayBuffer()).toEqual(buffer);
});
it("should send stream data", async () => {
const req = new node_1.Request(`${TEST_HTTP_URL}/echo`, {
method: "POST",
body: fs_1.createReadStream(path_1.join(__dirname, "../README.md")),
});
const res = await index_1.transport()(req, done);
expect(res.status).toEqual(200);
expect(res.statusText).toEqual("OK");
expect(res.headers.get("Content-Type")).toEqual("application/octet-stream");
expect(await res.text()).toContain("Popsicle Transport HTTP");
});
it("should abort before it starts", async () => {
const controller = new node_1.AbortController();
const req = new node_1.Request(`${TEST_HTTP_URL}/echo`, {
signal: controller.signal,
});
controller.abort();
expect.assertions(1);
try {
await index_1.transport()(req, done);
}
catch (err) {
if (err instanceof Error) {
expect(err.message).toEqual("Request has been aborted");
}
}
});
it("should abort mid-request", async () => {
const controller = new node_1.AbortController();
const req = new node_1.Request(`${TEST_HTTP_URL}/download`, controller);
const spy = jest.fn();
req.signal.on("responseBytes", spy);
const res = await index_1.transport()(req, done);
setTimeout(() => controller.abort(), 100);
expect(await res.text()).toEqual("hello ");
expect(spy).toBeCalledWith(6);
});
it("should abort mid-request with http2", async () => {
const controller = new node_1.AbortController();
const req = new node_1.Request(`${TEST_HTTP2_TLS_URL}/download`, controller);
const spy = jest.fn();
req.signal.on("responseBytes", spy);
const res = await index_1.transport({ rejectUnauthorized: false })(req, done);
setTimeout(() => controller.abort(), 100);
expect(await res.text()).toEqual("hello ");
expect(spy).toBeCalledWith(6);
});
it("should have no side effects aborting twice", async () => {
const controller = new node_1.AbortController();
const req = new node_1.Request(`${TEST_HTTP_URL}/download`, controller);
expect.assertions(1);
controller.abort();
controller.abort();
try {
await index_1.transport()(req, done);
}
catch (err) {
if (err instanceof Error) {
expect(err.message).toEqual("Request has been aborted");
}
}
});
it("should emit download progress", async () => {
const req = new node_1.Request(`${TEST_HTTP_URL}/download`);
const spy = jest.fn();
req.signal.on("responseBytes", spy);
const res = await index_1.transport()(req, done);
expect(await res.text()).toEqual("hello world!");
expect(spy).toBeCalledWith(12);
});
it("should re-use net socket", async () => {
const createNetConnection = jest.fn(index_1.defaultNetConnect);
const send = index_1.transport({
createNetConnection,
});
const req = new node_1.Request(TEST_HTTP_URL);
const res1 = await send(req, done);
expect(res1.status).toEqual(200);
expect(await res1.text()).toEqual("Success");
const res2 = await send(req, done);
expect(res2.status).toEqual(200);
expect(await res2.text()).toEqual("Success");
expect(createNetConnection).toBeCalledTimes(1);
});
it("should re-use net socket on free", async () => {
const netSockets = new index_1.SocketConnectionManager(Infinity, 1);
const createNetConnection = jest.fn(index_1.defaultNetConnect);
const send = index_1.transport({
netSockets,
createNetConnection,
});
const req = new node_1.Request(TEST_HTTP_URL);
const [res1, res2] = await Promise.all([send(req, done), send(req, done)]);
expect(res1.status).toEqual(200);
expect(res2.status).toEqual(200);
expect(createNetConnection).toBeCalledTimes(1);
});
it("should discard net socket at max connection", async () => {
const netSockets = new index_1.SocketConnectionManager(0);
const createNetConnection = jest.fn(index_1.defaultNetConnect);
const send = index_1.transport({
createNetConnection,
netSockets,
});
const req = new node_1.Request(TEST_HTTP_URL);
const res1 = await send(req, done);
expect(res1.status).toEqual(200);
expect(await res1.text()).toEqual("Success");
const res2 = await send(req, done);
expect(res2.status).toEqual(200);
expect(await res2.text()).toEqual("Success");
expect(createNetConnection).toBeCalledTimes(2);
});
it("should create new socket without keep alive", async () => {
const createNetConnection = jest.fn(index_1.defaultNetConnect);
const send = index_1.transport({
keepAlive: -1,
createNetConnection,
});
const req = new node_1.Request(TEST_HTTP_URL);
const res1 = await send(req, done);
expect(res1.status).toEqual(200);
expect(await res1.text()).toEqual("Success");
const res2 = await send(req, done);
expect(res2.status).toEqual(200);
expect(await res2.text()).toEqual("Success");
expect(createNetConnection).toBeCalledTimes(2);
});
it("should create new socket on free without keep alive", async () => {
const netSockets = new index_1.SocketConnectionManager(Infinity, 1);
const createNetConnection = jest.fn(index_1.defaultNetConnect);
const send = index_1.transport({
keepAlive: -1,
netSockets,
createNetConnection,
});
const req = new node_1.Request(TEST_HTTP_URL);
const [res1, res2] = await Promise.all([send(req, done), send(req, done)]);
expect(res1.status).toEqual(200);
expect(res2.status).toEqual(200);
expect(createNetConnection).toBeCalledTimes(2);
});
it("should reject https unauthorized", async () => {
expect.assertions(1);
try {
await index_1.transport()(new node_1.Request(TEST_HTTPS_URL), done);
}
catch (err) {
if (err instanceof index_1.ConnectionError) {
expect(err.code).toEqual("EUNAVAILABLE");
}
}
});
it.skip("should support https ca option", async () => {
const req = new node_1.Request(TEST_HTTPS_URL);
const res = await index_1.transport({ ca })(req, done);
expect(res.status).toEqual(200);
expect(await res.text()).toEqual("Success");
});
it("should support disabling reject unauthorized", async () => {
const req = new node_1.Request(TEST_HTTPS_URL);
const res = await index_1.transport({ rejectUnauthorized: false })(req, done);
expect(res.status).toEqual(200);
expect(await res.text()).toEqual("Success");
});
it("should re-use tls socket", async () => {
const createTlsConnection = jest.fn(index_1.defaultTlsConnect);
const send = index_1.transport({
rejectUnauthorized: false,
createTlsConnection,
});
const req = new node_1.Request(TEST_HTTPS_URL);
const res1 = await send(req, done);
expect(res1.status).toEqual(200);
expect(await res1.text()).toEqual("Success");
const res2 = await send(req, done);
expect(res2.status).toEqual(200);
expect(await res2.text()).toEqual("Success");
expect(createTlsConnection).toBeCalledTimes(1);
});
it("should connect to http2 non-tls server", async () => {
const req = new node_1.Request(TEST_HTTP2_URL);
const res = await index_1.transport({
negotiateHttpVersion: index_1.NegotiateHttpVersion.HTTP2_ONLY,
})(req, done);
expect(res.status).toEqual(200);
expect(res.httpVersion).toEqual("2.0");
expect(await res.text()).toEqual("Success");
});
it("should connect to http2 server", async () => {
const req = new node_1.Request(TEST_HTTP2_TLS_URL);
const res = await index_1.transport({ rejectUnauthorized: false })(req, done);
expect(res.status).toEqual(200);
expect(res.httpVersion).toEqual("2.0");
expect(await res.text()).toEqual("Success");
});
it("should connect to http2 server using http2 only", async () => {
const req = new node_1.Request(TEST_HTTP2_TLS_URL);
const res = await index_1.transport({
rejectUnauthorized: false,
negotiateHttpVersion: index_1.NegotiateHttpVersion.HTTP2_ONLY,
})(req, done);
expect(res.status).toEqual(200);
expect(res.httpVersion).toEqual("2.0");
expect(await res.text()).toEqual("Success");
});
it("should connect to https server using http1 only", async () => {
const req = new node_1.Request(TEST_HTTP2_TLS_URL);
const res = await index_1.transport({
rejectUnauthorized: false,
negotiateHttpVersion: index_1.NegotiateHttpVersion.HTTP1_ONLY,
})(req, done);
expect(res.status).toEqual(200);
expect(res.httpVersion).toEqual("1.1");
expect(await res.text()).toEqual("Success");
});
it("should re-use http2 client", async () => {
const createHttp2Connection = jest.fn(index_1.defaultHttp2Connect);
const send = index_1.transport({
rejectUnauthorized: false,
createHttp2Connection,
});
const req = new node_1.Request(TEST_HTTP2_TLS_URL);
const res1 = await send(req, done);
expect(res1.status).toEqual(200);
const res2 = await send(req, done);
expect(res2.status).toEqual(200);
expect(createHttp2Connection).toBeCalledTimes(1);
});
it("should re-use http2 client without tls", async () => {
const createHttp2Connection = jest.fn(index_1.defaultHttp2Connect);
const send = index_1.transport({
createHttp2Connection,
negotiateHttpVersion: index_1.NegotiateHttpVersion.HTTP2_ONLY,
});
const req = new node_1.Request(TEST_HTTP2_URL);
const res1 = await send(req, done);
expect(res1.status).toEqual(200);
const res2 = await send(req, done);
expect(res2.status).toEqual(200);
expect(createHttp2Connection).toBeCalledTimes(1);
});
it("should re-use connection to http2 server", async () => {
const createHttp2Connection = jest.fn(index_1.defaultHttp2Connect);
const send = index_1.transport({
rejectUnauthorized: false,
createHttp2Connection,
});
const req = new node_1.Request(TEST_HTTP2_TLS_URL);
const [res1, res2] = await Promise.all([send(req, done), send(req, done)]);
expect(res1.status).toEqual(200);
expect(res2.status).toEqual(200);
expect(createHttp2Connection).toBeCalledTimes(1);
});
it("should allow custom dns lookup for http requests", async () => {
const lookup = jest.fn((hostname, options, callback) => callback(null, "127.0.0.1", 4));
const send = index_1.transport({
rejectUnauthorized: false,
lookup,
});
const req = new node_1.Request(`${TEST_HTTP_URL}/status/200`);
const res = await send(req, done);
expect(res.status).toEqual(200);
expect(lookup).toBeCalledTimes(1);
});
it("should allow custom dns lookup for https requests", async () => {
const lookup = jest.fn((hostname, options, callback) => callback(null, "127.0.0.1", 4));
const send = index_1.transport({
rejectUnauthorized: false,
lookup,
});
const req = new node_1.Request(`${TEST_HTTPS_URL}/status/200`);
const res = await send(req, done);
expect(res.status).toEqual(200);
expect(lookup).toBeCalledTimes(1);
});
it("should allow custom dns lookup for http2 requests", async () => {
const lookup = jest.fn((hostname, options, callback) => callback(null, "127.0.0.1", 4));
const send = index_1.transport({
rejectUnauthorized: false,
lookup,
});
const req = new node_1.Request(TEST_HTTP2_TLS_URL);
const res = await send(req, done);
expect(res.status).toEqual(200);
expect(lookup).toBeCalledTimes(1);
});
for (const url of ALL_URLS) {
it(`should handle connection issues to ${url}`, async () => {
expect.assertions(1);
try {
await index_1.transport({
rejectUnauthorized: false,
negotiateHttpVersion: url === TEST_HTTP2_URL
? index_1.NegotiateHttpVersion.HTTP2_ONLY
: undefined,
})(new node_1.Request(`${url}/close`), done);
}
catch (err) {
if (err instanceof index_1.ConnectionError) {
expect(err.code).toEqual("EUNAVAILABLE");
}
}
});
it(`should handle request timeouts to ${url}`, async () => {
const createNetConnection = jest.fn(index_1.defaultNetConnect);
const createTlsConnection = jest.fn(index_1.defaultTlsConnect);
const createHttp2Connection = jest.fn(index_1.defaultHttp2Connect);
const send = index_1.transport({
idleRequestTimeout: 50,
rejectUnauthorized: false,
negotiateHttpVersion: url === TEST_HTTP2_URL || url === TEST_HTTP2_TLS_URL
? index_1.NegotiateHttpVersion.HTTP2_ONLY
: undefined,
createNetConnection,
createTlsConnection,
createHttp2Connection,
});
expect.assertions(5);
try {
await send(new node_1.Request(`${url}/timeout`), done);
}
catch (err) {
if (err instanceof index_1.ConnectionError) {
expect(err.code).toEqual("EUNAVAILABLE");
expect(err.cause).toBeInstanceOf(index_1.CausedByTimeoutError);
}
}
// Test connection re-use from previous step without timeout.
const res = await send(new node_1.Request(url), done);
expect(await res.text()).toEqual("Success");
switch (url) {
case TEST_HTTP2_TLS_URL:
expect(res.httpVersion).toEqual("2.0");
expect(createTlsConnection).toBeCalledTimes(1);
break;
case TEST_HTTP2_URL:
expect(res.httpVersion).toEqual("2.0");
expect(createNetConnection).toBeCalledTimes(1);
break;
case TEST_HTTPS_URL:
expect(res.httpVersion).toEqual("1.1");
expect(createTlsConnection).toBeCalledTimes(2);
break;
case TEST_HTTP_URL:
expect(res.httpVersion).toEqual("1.1");
expect(createNetConnection).toBeCalledTimes(2);
break;
}
});
it(`should handle socket timeouts to ${url}`, async () => {
const createNetConnection = jest.fn(index_1.defaultNetConnect);
const createTlsConnection = jest.fn(index_1.defaultTlsConnect);
const createHttp2Connection = jest.fn(index_1.defaultHttp2Connect);
const send = index_1.transport({
idleSocketTimeout: 50,
rejectUnauthorized: false,
negotiateHttpVersion: url === TEST_HTTP2_URL || url === TEST_HTTP2_TLS_URL
? index_1.NegotiateHttpVersion.HTTP2_ONLY
: undefined,
createNetConnection,
createTlsConnection,
createHttp2Connection,
});
expect.assertions(4);
const res1 = await send(new node_1.Request(url), done);
expect(await res1.text()).toEqual("Success");
// Node.js v10 has trouble with the timeout being executed on time here.
await wait(process.version.startsWith("v10.") ? 51 : 50);
const res2 = await send(new node_1.Request(url), done);
expect(await res2.text()).toEqual("Success");
switch (url) {
case TEST_HTTP2_TLS_URL:
expect(res2.httpVersion).toEqual("2.0");
expect(createTlsConnection).toBeCalledTimes(2);
break;
case TEST_HTTP2_URL:
expect(res2.httpVersion).toEqual("2.0");
expect(createNetConnection).toBeCalledTimes(2);
break;
case TEST_HTTPS_URL:
expect(res2.httpVersion).toEqual("1.1");
expect(createTlsConnection).toBeCalledTimes(2);
break;
case TEST_HTTP_URL:
expect(res2.httpVersion).toEqual("1.1");
expect(createNetConnection).toBeCalledTimes(2);
break;
}
});
it(`should handle socket timeouts during a request to ${url}`, async () => {
const createNetConnection = jest.fn(index_1.defaultNetConnect);
const createTlsConnection = jest.fn(index_1.defaultTlsConnect);
const createHttp2Connection = jest.fn(index_1.defaultHttp2Connect);
const send = index_1.transport({
idleSocketTimeout: 50,
rejectUnauthorized: false,
negotiateHttpVersion: url === TEST_HTTP2_URL || url === TEST_HTTP2_TLS_URL
? index_1.NegotiateHttpVersion.HTTP2_ONLY
: undefined,
createNetConnection,
createTlsConnection,
createHttp2Connection,
});
expect.assertions(5);
try {
await send(new node_1.Request(`${url}/timeout`), done);
}
catch (err) {
if (err instanceof index_1.ConnectionError) {
expect(err.code).toEqual("EUNAVAILABLE");
expect(err.cause).toBeInstanceOf(index_1.CausedByTimeoutError);
}
}
// Test connection re-use from previous step without timeout.
const res = await send(new node_1.Request(url), done);
expect(await res.text()).toEqual("Success");
switch (url) {
case TEST_HTTP2_TLS_URL:
expect(res.httpVersion).toEqual("2.0");
expect(createTlsConnection).toBeCalledTimes(2);
break;
case TEST_HTTP2_URL:
expect(res.httpVersion).toEqual("2.0");
expect(createNetConnection).toBeCalledTimes(2);
break;
case TEST_HTTPS_URL:
expect(res.httpVersion).toEqual("1.1");
expect(createTlsConnection).toBeCalledTimes(2);
break;
case TEST_HTTP_URL:
expect(res.httpVersion).toEqual("1.1");
expect(createNetConnection).toBeCalledTimes(2);
break;
}
});
}
});
//# sourceMappingURL=index.spec.js.map