blob: 413087435ff6782873f0a470520407286cebaeb6 [file]
// Copyright 2021-2023 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as http2 from "http2";
import * as http from "http";
import { DubboError } from "@apachedubbo/dubbo";
import { createAsyncIterable } from "@apachedubbo/dubbo/protocol";
import { createNodeHttpClient } from "./node-universal-client.js";
import { useNodeServer } from "./use-node-server-helper.spec.js";
// Polyfill the Headers API for Node versions < 18
import "./node-headers-polyfill.js";
describe("node http/2 client closing with RST_STREAM with code CANCEL", function () {
let serverReceivedRstCode: number | undefined;
const server = useNodeServer(() =>
http2.createServer().on("stream", (stream) => {
stream.on("close", () => (serverReceivedRstCode = stream.rstCode));
})
);
it("should send RST_STREAM frame to the server", async function () {
return new Promise<void>((resolve) => {
http2.connect(server.getUrl(), (session: http2.ClientHttp2Session) => {
const stream = session.request(
{
":method": "POST",
":path": "/",
},
{}
);
setTimeout(() => {
stream.close(http2.constants.NGHTTP2_CANCEL, () => {
// We are seeing a race condition in Node v16.20.0, where closing
// the session right after closing a stream with an RST code
// _sometimes_ sends an INTERNAL_ERROR code.
// Simply delaying the session close until the next tick like
// we do here seems to work around the issue.
// We do _not_ guard against this case in the universal client,
// since we were not able to reproduce the issue there.
setTimeout(() => session.close(resolve), 0);
});
}, 0);
});
});
while (serverReceivedRstCode === undefined) {
// wait for the server to see the reset code
await new Promise((resolve) => setTimeout(resolve, 1));
}
expect(serverReceivedRstCode).toBe(http2.constants.NGHTTP2_CANCEL);
});
});
describe("universal node http client", function () {
describe("against an unresolvable host", function () {
for (const httpVersion of ["2", "1.1"] as const) {
describe(`over http ${httpVersion}`, function () {
it("should raise Code.Unavailable", async function () {
const client = createNodeHttpClient({
httpVersion,
});
try {
await client({
url: "https://unresolvable-host.some.domain",
method: "POST",
header: new Headers(),
});
} catch (e) {
expect(DubboError.from(e).message).toBe(
"[unavailable] getaddrinfo ENOTFOUND unresolvable-host.some.domain"
);
}
});
});
}
});
describe("against a server that closes immediately", function () {
describe("over http/2", function () {
let serverReceivedRequest = false;
const server = useNodeServer(() =>
http2.createServer((request, response) => {
serverReceivedRequest = true;
response.stream.close(http2.constants.NGHTTP2_CANCEL);
})
);
it("should reject the response promise with Code.Canceled", async function () {
const client = server.getClient();
try {
await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
});
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe(
"[canceled] http/2 stream closed with error code CANCEL (0x8)"
);
}
expect(serverReceivedRequest).toBeTrue();
});
});
describe("over http/1.1", function () {
let serverReceivedRequest = false;
const server = useNodeServer(() =>
http.createServer((req, res) => {
serverReceivedRequest = true;
res.destroy();
})
);
it("should reject the response promise", async function () {
const client = server.getClient();
try {
await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
});
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe("[aborted] socket hang up");
}
expect(serverReceivedRequest).toBeTrue();
});
});
});
describe("against a server that closes before the first response byte", function () {
describe("over http/2", function () {
const server = useNodeServer(() =>
http2.createServer((req, res) => {
res.writeHead(200);
// Calling close in the same tick as writeHead appears to prevent
// headers from being sent. The client response promise will reject,
// instead of the response body.
setTimeout(() => res.stream.close(http2.constants.NGHTTP2_CANCEL), 0);
})
);
it("should reject the response promise with Code.Canceled", async function () {
const client = server.getClient();
const res = await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
});
try {
for await (const chunk of res.body) {
expect(chunk)
.withContext("response body iterable should be empty")
.toBeUndefined();
}
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe(
"[canceled] http/2 stream closed with error code CANCEL (0x8)"
);
}
});
});
describe("over http/1.1", function () {
const server = useNodeServer(() =>
http.createServer((req, res) => {
res.writeHead(200);
res.flushHeaders();
res.destroy();
})
);
it("should reject the response promise", async function () {
const client = server.getClient();
const res = await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
});
try {
for await (const chunk of res.body) {
expect(chunk)
.withContext("response body iterable should be empty")
.toBeUndefined();
}
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe("[aborted] aborted");
}
});
});
});
describe("against a server that closes mid request", function () {
describe("over http/2", function () {
let serverReceivedBytes = 0;
const server = useNodeServer(() =>
http2.createServer((req, res) => {
void (async () => {
for await (const chunk of req) {
serverReceivedBytes += (chunk as Uint8Array).byteLength;
res.stream.close(http2.constants.NGHTTP2_CANCEL);
break;
}
})();
})
);
it("should reject the response promise with Code.Canceled", async function () {
const client = server.getClient();
async function* body() {
yield new Uint8Array(32);
await new Promise<void>(() => {
// never resolves
});
}
try {
await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
body: body(),
});
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe(
"[canceled] http/2 stream closed with error code CANCEL (0x8)"
);
}
expect(serverReceivedBytes).toBe(32);
});
});
describe("over http/1.1", function () {
let serverReceivedBytes = 0;
const server = useNodeServer(() =>
http.createServer((req, res) => {
void (async () => {
for await (const chunk of req) {
serverReceivedBytes += (chunk as Uint8Array).byteLength;
res.destroy();
break;
}
})();
})
);
it("should reject the response promise", async function () {
const client = server.getClient();
async function* body() {
yield new Uint8Array(32);
await new Promise<void>(() => {
// never resolves
});
}
try {
await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
body: body(),
});
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe("[aborted] socket hang up");
}
expect(serverReceivedBytes).toBe(32);
});
});
});
describe("against a server that closes mid response", function () {
describe("over http/2", function () {
let serverSentBytes = 0;
const server = useNodeServer(() =>
http2.createServer((req, res) => {
void (async () => {
res.writeHead(200);
await new Promise<void>((resolve, reject) =>
res.write(new Uint8Array(64), (e: Error | undefined) =>
e ? reject(e) : resolve()
)
);
serverSentBytes += 64;
res.stream.close(http2.constants.NGHTTP2_CANCEL);
})();
})
);
it("should reject the response promise with Code.Canceled", async function () {
const client = server.getClient();
const res = await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
});
try {
for await (const chunk of res.body) {
expect(chunk.byteLength).toBe(64);
}
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe(
"[canceled] http/2 stream closed with error code CANCEL (0x8)"
);
}
expect(serverSentBytes).toBe(64);
});
});
describe("over http/1.1", function () {
let serverSentBytes = 0;
const server = useNodeServer(() =>
http.createServer((req, res) => {
void (async () => {
res.writeHead(200);
await new Promise<void>((resolve, reject) =>
res.write(new Uint8Array(64), (e) => (e ? reject(e) : resolve()))
);
serverSentBytes += 64;
res.destroy();
})();
})
);
it("should reject the response promise", async function () {
const client = server.getClient();
const res = await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
});
try {
for await (const chunk of res.body) {
expect(chunk.byteLength).toBe(64);
}
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toMatch(
/\[aborted] (aborted|read ECONNRESET)/
);
}
expect(serverSentBytes).toBe(64);
});
});
});
describe("with a signal that is already aborted", function () {
describe("over http/2", function () {
let serverReceivedRequest = false;
const server = useNodeServer(() =>
http2.createServer(() => (serverReceivedRequest = true))
);
it("should raise error with Code.Canceled and never hit the server", async function () {
const client = server.getClient();
const signal = AbortSignal.abort();
// client should raise error
try {
await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
signal,
});
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe(
"[canceled] This operation was aborted"
);
}
// request should never hit the server
expect(serverReceivedRequest).toBeFalse();
});
});
describe("over http/1.1", function () {
let serverReceivedRequest = false;
const server = useNodeServer(() =>
http.createServer(() => (serverReceivedRequest = true))
);
it("should raise error with Code.Canceled and never hit the server", async function () {
const client = server.getClient();
const signal = AbortSignal.abort();
// client should raise error
try {
await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
signal,
});
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe(
"[canceled] This operation was aborted"
);
}
// request should never hit the server
expect(serverReceivedRequest).toBeFalse();
});
});
});
describe("with a signal aborting before first request byte", function () {
describe("over http/2", function () {
let serverReceivedRstCode: number | undefined;
let serverReceivedBytes = 0;
const server = useNodeServer(() =>
http2.createServer((req, res) => {
res.stream.on(
"close",
() => (serverReceivedRstCode = res.stream.rstCode)
);
void (async () => {
for await (const chunk of req) {
serverReceivedBytes += (chunk as Uint8Array).byteLength;
}
})();
})
);
it("should raise error with code canceled and send RST_STREAM with code CANCEL", async function () {
// set up a client that aborts while still streaming the request body
const client = server.getClient();
const ac = new AbortController();
async function* body() {
await new Promise<void>((resolve) => setTimeout(resolve, 50));
ac.abort();
yield new Uint8Array(32);
}
// client should raise error
try {
await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
body: body(),
signal: ac.signal,
});
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe(
"[canceled] This operation was aborted"
);
}
// server should receive chunks until client cancelled
while (serverReceivedRstCode === undefined) {
// wait for the server to see the reset code
await new Promise((resolve) => setTimeout(resolve, 1));
}
expect(serverReceivedRstCode).toBe(http2.constants.NGHTTP2_CANCEL);
expect(serverReceivedBytes).toBe(0);
});
});
describe("over http/1.1", function () {
let serverReceivedRequest = false;
let serverReceivedBytes = 0;
let serverRequestClosed = false;
let serverResponseClosed = false;
let serverRequestEmittedAborted = false;
let serverRequestEmittedError: (Error & { code?: string }) | undefined;
let serverRequestIterableErrored: (Error & { code?: string }) | undefined;
const server = useNodeServer(() =>
http.createServer((req, res) => {
serverReceivedRequest = true;
req.on("aborted", () => (serverRequestEmittedAborted = true));
req.on("error", (e) => (serverRequestEmittedError = e));
req.on("close", () => (serverRequestClosed = true));
res.on("close", () => (serverResponseClosed = true));
void (async () => {
try {
for await (const chunk of req) {
serverReceivedBytes += (chunk as Uint8Array).byteLength;
}
} catch (e) {
serverRequestIterableErrored = e as Error & { code?: string };
}
})();
})
);
it("should raise error with code canceled", async function () {
// set up a client that aborts while still streaming the request body
const client = server.getClient();
const ac = new AbortController();
async function* body() {
await new Promise<void>((resolve) => setTimeout(resolve, 50));
ac.abort();
yield new Uint8Array(32);
}
// client should raise error
try {
await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
body: body(),
signal: ac.signal,
});
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe(
"[canceled] This operation was aborted"
);
}
// server should receive chunks until client cancelled
expect(serverReceivedRequest)
.withContext("serverReceivedRequest")
.toBeTrue();
while (!serverResponseClosed) {
// wait for the server to see the response being closed
await new Promise((resolve) => setTimeout(resolve, 1));
}
expect(serverRequestClosed).toBeTrue();
expect(serverResponseClosed).toBeTrue();
expect(serverRequestEmittedAborted).toBeTrue();
expect(serverRequestEmittedError?.code).toBe("ECONNRESET");
expect(serverRequestIterableErrored?.code).toBe("ECONNRESET");
expect(serverReceivedBytes).toBe(0);
});
});
});
describe("with a signal aborting mid request", function () {
describe("over http/2", function () {
let serverReceivedRstCode: number | undefined;
let serverReceivedBytes = 0;
const server = useNodeServer(() =>
http2.createServer((req, res) => {
res.stream.on(
"close",
() => (serverReceivedRstCode = res.stream.rstCode)
);
void (async () => {
for await (const chunk of req) {
serverReceivedBytes += (chunk as Uint8Array).byteLength;
}
})();
})
);
it("should raise error with code canceled and send RST_STREAM with code CANCEL", async function () {
// set up a client that aborts while still streaming the request body
const client = server.getClient();
const ac = new AbortController();
async function* body() {
yield new Uint8Array(32);
await new Promise<void>((resolve) => setTimeout(resolve, 50));
ac.abort();
yield new Uint8Array(32);
}
// client should raise error
try {
await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
body: body(),
signal: ac.signal,
});
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe(
"[canceled] This operation was aborted"
);
}
// server should receive chunks until client cancelled
while (serverReceivedRstCode === undefined) {
// wait for the server to see the reset code
await new Promise((resolve) => setTimeout(resolve, 1));
}
expect(serverReceivedRstCode).toBe(http2.constants.NGHTTP2_CANCEL);
expect(serverReceivedBytes).toBe(32);
});
});
describe("over http/1.1", function () {
let serverReceivedRequest = false;
let serverReceivedBytes = 0;
let serverRequestClosed = false;
let serverResponseClosed = false;
let serverRequestEmittedAborted = false;
let serverRequestEmittedError: (Error & { code?: string }) | undefined;
let serverRequestIterableErrored: (Error & { code?: string }) | undefined;
const server = useNodeServer(() =>
http.createServer((req, res) => {
serverReceivedRequest = true;
req.on("aborted", () => (serverRequestEmittedAborted = true));
req.on("error", (e) => (serverRequestEmittedError = e));
req.on("close", () => (serverRequestClosed = true));
res.on("close", () => (serverResponseClosed = true));
void (async () => {
try {
for await (const chunk of req) {
serverReceivedBytes += (chunk as Uint8Array).byteLength;
}
} catch (e) {
serverRequestIterableErrored = e as Error & { code?: string };
}
})();
})
);
it("should raise error with code canceled", async function () {
// set up a client that aborts while still streaming the request body
const client = server.getClient();
const ac = new AbortController();
async function* body() {
yield new Uint8Array(32);
await new Promise<void>((resolve) => setTimeout(resolve, 50));
ac.abort();
yield new Uint8Array(32);
}
// client should raise error
try {
await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
body: body(),
signal: ac.signal,
});
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe(
"[canceled] This operation was aborted"
);
}
// server should receive chunks until client cancelled
expect(serverReceivedRequest).toBeTrue();
while (!serverResponseClosed) {
// wait for the server to see the response being closed
await new Promise((resolve) => setTimeout(resolve, 1));
}
expect(serverRequestClosed).toBeTrue();
expect(serverResponseClosed).toBeTrue();
expect(serverRequestEmittedAborted).toBeTrue();
expect(serverRequestEmittedError?.code).toBe("ECONNRESET");
expect(serverRequestIterableErrored?.code).toBe("ECONNRESET");
expect(serverReceivedBytes).toBe(32);
});
});
});
describe("with a signal aborting mid response", function () {
describe("over http/2", function () {
let serverReceivedRstCode: number | undefined;
let serverSentBytes = 0;
const server = useNodeServer(() =>
http2.createServer((req, res) => {
res.stream.on(
"close",
() => (serverReceivedRstCode = res.stream.rstCode)
);
void (async () => {
res.writeHead(200);
res.write(new Uint8Array(64));
serverSentBytes += 64;
await new Promise<void>(() => {
// never resolves
});
})();
})
);
it("should raise error with code canceled and send RST_STREAM with code CANCEL", async function () {
// set up a client that aborts while still streaming the request body
const client = server.getClient();
const ac = new AbortController();
const res = await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
body: createAsyncIterable([]),
signal: ac.signal,
});
// should raise error with code canceled
try {
for await (const chunk of res.body) {
expect(chunk.byteLength).toBe(64);
ac.abort();
}
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe(
"[canceled] This operation was aborted"
);
}
// server should receive RST_STREAM with code CANCEL
while (serverReceivedRstCode === undefined) {
// wait for the server to see the reset code
await new Promise((resolve) => setTimeout(resolve, 1));
}
expect(serverReceivedRstCode).toBe(http2.constants.NGHTTP2_CANCEL);
expect(serverSentBytes).toBe(64);
});
});
describe("over http/1.1", function () {
let serverSentBytes = 0;
let serverRequestClosed = false;
let serverResponseClosed = false;
const server = useNodeServer(() =>
http.createServer((req, res) => {
req.on("close", () => (serverRequestClosed = true));
res.on("close", () => (serverResponseClosed = true));
void (async () => {
res.writeHead(200);
res.write(new Uint8Array(64));
serverSentBytes += 64;
await new Promise<void>(() => {
// never resolves
});
})();
})
);
it("should raise error with code canceled", async function () {
// set up a client that aborts while still streaming the request body
const client = server.getClient();
const ac = new AbortController();
const res = await client({
url: server.getUrl(),
method: "POST",
header: new Headers(),
body: createAsyncIterable([]),
signal: ac.signal,
});
// should raise error with code canceled
try {
for await (const chunk of res.body) {
expect(chunk.byteLength).toBe(64);
ac.abort();
}
fail("expected error");
} catch (e) {
expect(e).toBeInstanceOf(DubboError);
expect(DubboError.from(e).message).toBe(
"[canceled] This operation was aborted"
);
}
// server should see request close
while (!serverResponseClosed) {
// wait for the server to see the response being closed
await new Promise((resolve) => setTimeout(resolve, 1));
}
expect(serverRequestClosed).toBeTrue();
expect(serverResponseClosed).toBeTrue();
expect(serverSentBytes).toBe(64);
});
});
});
});