mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Fix HTTP spec issues by upgrading uWS version (#14853)
This commit is contained in:
@@ -16,8 +16,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
// clang-format off
|
||||
#ifndef UWS_APP_H
|
||||
#define UWS_APP_H
|
||||
|
||||
|
||||
#include <string>
|
||||
#include <charconv>
|
||||
@@ -619,4 +618,3 @@ typedef TemplatedApp<true> SSLApp;
|
||||
|
||||
}
|
||||
|
||||
#endif // UWS_APP_H
|
||||
@@ -16,8 +16,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef UWS_HTTPCONTEXT_H
|
||||
#define UWS_HTTPCONTEXT_H
|
||||
#pragma once
|
||||
|
||||
/* This class defines the main behavior of HTTP and emits various events */
|
||||
|
||||
@@ -27,6 +26,8 @@
|
||||
#include "AsyncSocket.h"
|
||||
#include "WebSocketData.h"
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <string_view>
|
||||
#include <iostream>
|
||||
#include "MoveOnlyFunction.h"
|
||||
@@ -171,7 +172,7 @@ private:
|
||||
#endif
|
||||
|
||||
/* The return value is entirely up to us to interpret. The HttpParser only care for whether the returned value is DIFFERENT or not from passed user */
|
||||
void *returnedSocket = httpResponseData->consumePostPadded(data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
|
||||
auto [err, returnedSocket] = httpResponseData->consumePostPadded(data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
|
||||
/* For every request we reset the timeout and hang until user makes action */
|
||||
/* Warning: if we are in shutdown state, resetting the timer is a security issue! */
|
||||
us_socket_timeout(SSL, (us_socket_t *) s, 0);
|
||||
@@ -180,7 +181,9 @@ private:
|
||||
HttpResponseData<SSL> *httpResponseData = (HttpResponseData<SSL> *) us_socket_ext(SSL, (us_socket_t *) s);
|
||||
httpResponseData->offset = 0;
|
||||
|
||||
/* Are we not ready for another request yet? Terminate the connection. */
|
||||
/* Are we not ready for another request yet? Terminate the connection.
|
||||
* Important for denying async pipelining until, if ever, we want to suppot it.
|
||||
* Otherwise requests can get mixed up on the same connection. We still support sync pipelining. */
|
||||
if (httpResponseData->state & HttpResponseData<SSL>::HTTP_RESPONSE_PENDING) {
|
||||
us_socket_close(SSL, (us_socket_t *) s, 0, nullptr);
|
||||
return nullptr;
|
||||
@@ -280,10 +283,6 @@ private:
|
||||
}
|
||||
}
|
||||
return user;
|
||||
}, [](void *user) {
|
||||
/* Close any socket on HTTP errors */
|
||||
us_socket_close(SSL, (us_socket_t *) user, 0, nullptr);
|
||||
return nullptr;
|
||||
});
|
||||
|
||||
/* Mark that we are no longer parsing Http */
|
||||
@@ -291,6 +290,9 @@ private:
|
||||
|
||||
/* If we got fullptr that means the parser wants us to close the socket from error (same as calling the errorHandler) */
|
||||
if (returnedSocket == FULLPTR) {
|
||||
/* For errors, we only deliver them "at most once". We don't care if they get halfways delivered or not. */
|
||||
us_socket_write(SSL, s, httpErrorResponses[err].data(), (int) httpErrorResponses[err].length(), false);
|
||||
us_socket_shutdown(SSL, s);
|
||||
/* Close any socket on HTTP errors */
|
||||
us_socket_close(SSL, s, 0, nullptr);
|
||||
/* This just makes the following code act as if the socket was closed from error inside the parser. */
|
||||
@@ -299,9 +301,8 @@ private:
|
||||
|
||||
/* We need to uncork in all cases, except for nullptr (closed socket, or upgraded socket) */
|
||||
if (returnedSocket != nullptr) {
|
||||
us_socket_t* returnedSocketPtr = (us_socket_t*) returnedSocket;
|
||||
/* We don't want open sockets to keep the event loop alive between HTTP requests */
|
||||
us_socket_unref(returnedSocketPtr);
|
||||
us_socket_unref((us_socket_t *) returnedSocket);
|
||||
|
||||
/* Timeout on uncork failure */
|
||||
auto [written, failed] = ((AsyncSocket<SSL> *) returnedSocket)->uncork();
|
||||
@@ -321,7 +322,7 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnedSocketPtr;
|
||||
return (us_socket_t *) returnedSocket;
|
||||
}
|
||||
|
||||
/* If we upgraded, check here (differ between nullptr close and nullptr upgrade) */
|
||||
@@ -483,10 +484,27 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
httpContextData->currentRouter->add(methods, pattern, [handler = std::move(handler)](auto *r) mutable {
|
||||
/* Record this route's parameter offsets */
|
||||
std::map<std::string, unsigned short, std::less<>> parameterOffsets;
|
||||
unsigned short offset = 0;
|
||||
for (unsigned int i = 0; i < pattern.length(); i++) {
|
||||
if (pattern[i] == ':') {
|
||||
i++;
|
||||
unsigned int start = i;
|
||||
while (i < pattern.length() && pattern[i] != '/') {
|
||||
i++;
|
||||
}
|
||||
parameterOffsets[std::string(pattern.data() + start, i - start)] = offset;
|
||||
//std::cout << "<" << std::string(pattern.data() + start, i - start) << "> is offset " << offset;
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
|
||||
httpContextData->currentRouter->add(methods, pattern, [handler = std::move(handler), parameterOffsets = std::move(parameterOffsets)](auto *r) mutable {
|
||||
auto user = r->getUserData();
|
||||
user.httpRequest->setYield(false);
|
||||
user.httpRequest->setParameters(r->getParameters());
|
||||
user.httpRequest->setParameterOffsets(¶meterOffsets);
|
||||
|
||||
/* Middleware? Automatically respond to expectations */
|
||||
std::string_view expect = user.httpRequest->getHeader("expect");
|
||||
@@ -528,4 +546,4 @@ public:
|
||||
|
||||
}
|
||||
|
||||
#endif // UWS_HTTPCONTEXT_H
|
||||
|
||||
|
||||
53
packages/bun-uws/src/HttpError.h
Normal file
53
packages/bun-uws/src/HttpError.h
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Authored by Alex Hultman, 2018-2023.
|
||||
* Intellectual property of third-party.
|
||||
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef UWS_HTTP_ERRORS
|
||||
#define UWS_HTTP_ERRORS
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace uWS {
|
||||
/* Possible errors from http parsing */
|
||||
enum HttpError {
|
||||
HTTP_ERROR_505_HTTP_VERSION_NOT_SUPPORTED = 1,
|
||||
HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 2,
|
||||
HTTP_ERROR_400_BAD_REQUEST = 3
|
||||
};
|
||||
|
||||
#ifndef UWS_HTTPRESPONSE_NO_WRITEMARK
|
||||
|
||||
/* Returned parser errors match this LUT. */
|
||||
static const std::string_view httpErrorResponses[] = {
|
||||
"", /* Zeroth place is no error so don't use it */
|
||||
"HTTP/1.1 505 HTTP Version Not Supported\r\nConnection: close\r\n\r\n<h1>HTTP Version Not Supported</h1><p>This server does not support HTTP/1.0.</p><hr><i>uWebSockets/20 Server</i>",
|
||||
"HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n<h1>Request Header Fields Too Large</h1><hr><i>uWebSockets/20 Server</i>",
|
||||
"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n<h1>Bad Request</h1><hr><i>uWebSockets/20 Server</i>",
|
||||
};
|
||||
|
||||
#else
|
||||
/* Anonymized pages */
|
||||
static const std::string_view httpErrorResponses[] = {
|
||||
"", /* Zeroth place is no error so don't use it */
|
||||
"HTTP/1.1 505 HTTP Version Not Supported\r\nConnection: close\r\n\r\n",
|
||||
"HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n",
|
||||
"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"
|
||||
};
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
42
packages/bun-uws/src/HttpErrors.h
Normal file
42
packages/bun-uws/src/HttpErrors.h
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Authored by Alex Hultman, 2018-2023.
|
||||
* Intellectual property of third-party.
|
||||
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace uWS {
|
||||
/* Possible errors from http parsing */
|
||||
enum HttpError {
|
||||
HTTP_ERROR_505_HTTP_VERSION_NOT_SUPPORTED = 1,
|
||||
HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 2,
|
||||
HTTP_ERROR_400_BAD_REQUEST = 3
|
||||
};
|
||||
|
||||
|
||||
/* Anonymized pages */
|
||||
static const std::string_view httpErrorResponses[] = {
|
||||
"", /* Zeroth place is no error so don't use it */
|
||||
"HTTP/1.1 505 HTTP Version Not Supported\r\nConnection: close\r\n\r\n",
|
||||
"HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n",
|
||||
"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"
|
||||
};
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,8 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
// clang-format off
|
||||
#ifndef UWS_HTTPRESPONSEDATA_H
|
||||
#define UWS_HTTPRESPONSEDATA_H
|
||||
#pragma once
|
||||
|
||||
/* This data belongs to the HttpResponse */
|
||||
|
||||
@@ -106,4 +105,4 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
|
||||
|
||||
}
|
||||
|
||||
#endif // UWS_HTTPRESPONSEDATA_H
|
||||
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef UWS_TOPICTREE_H
|
||||
#define UWS_TOPICTREE_H
|
||||
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <list>
|
||||
#include <iostream>
|
||||
@@ -366,4 +364,4 @@ public:
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
341
test/harness.ts
341
test/harness.ts
@@ -307,174 +307,174 @@ const binaryTypes = {
|
||||
"float32array": Float32Array,
|
||||
"float64array": Float64Array,
|
||||
} as const;
|
||||
|
||||
expect.extend({
|
||||
toHaveTestTimedOutAfter(actual: any, expected: number) {
|
||||
if (typeof actual !== "string") {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected ${actual} to be a string`,
|
||||
};
|
||||
}
|
||||
|
||||
const preStartI = actual.indexOf("timed out after ");
|
||||
if (preStartI === -1) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected ${actual} to contain "timed out after "`,
|
||||
};
|
||||
}
|
||||
const startI = preStartI + "timed out after ".length;
|
||||
const endI = actual.indexOf("ms", startI);
|
||||
if (endI === -1) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected ${actual} to contain "ms" after "timed out after "`,
|
||||
};
|
||||
}
|
||||
const int = parseInt(actual.slice(startI, endI));
|
||||
if (!Number.isSafeInteger(int)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected ${int} to be a safe integer`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pass: int >= expected,
|
||||
message: () => `Expected ${int} to be >= ${expected}`,
|
||||
};
|
||||
},
|
||||
toBeBinaryType(actual: any, expected: keyof typeof binaryTypes) {
|
||||
switch (expected) {
|
||||
case "buffer":
|
||||
if (expect.extend)
|
||||
expect.extend({
|
||||
toHaveTestTimedOutAfter(actual: any, expected: number) {
|
||||
if (typeof actual !== "string") {
|
||||
return {
|
||||
pass: Buffer.isBuffer(actual),
|
||||
message: () => `Expected ${actual} to be buffer`,
|
||||
pass: false,
|
||||
message: () => `Expected ${actual} to be a string`,
|
||||
};
|
||||
case "arraybuffer":
|
||||
}
|
||||
|
||||
const preStartI = actual.indexOf("timed out after ");
|
||||
if (preStartI === -1) {
|
||||
return {
|
||||
pass: actual instanceof ArrayBuffer,
|
||||
message: () => `Expected ${actual} to be ArrayBuffer`,
|
||||
pass: false,
|
||||
message: () => `Expected ${actual} to contain "timed out after "`,
|
||||
};
|
||||
default: {
|
||||
const ctor = binaryTypes[expected];
|
||||
if (!ctor) {
|
||||
}
|
||||
const startI = preStartI + "timed out after ".length;
|
||||
const endI = actual.indexOf("ms", startI);
|
||||
if (endI === -1) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected ${actual} to contain "ms" after "timed out after "`,
|
||||
};
|
||||
}
|
||||
const int = parseInt(actual.slice(startI, endI));
|
||||
if (!Number.isSafeInteger(int)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected ${int} to be a safe integer`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pass: int >= expected,
|
||||
message: () => `Expected ${int} to be >= ${expected}`,
|
||||
};
|
||||
},
|
||||
toBeBinaryType(actual: any, expected: keyof typeof binaryTypes) {
|
||||
switch (expected) {
|
||||
case "buffer":
|
||||
return {
|
||||
pass: Buffer.isBuffer(actual),
|
||||
message: () => `Expected ${actual} to be buffer`,
|
||||
};
|
||||
case "arraybuffer":
|
||||
return {
|
||||
pass: actual instanceof ArrayBuffer,
|
||||
message: () => `Expected ${actual} to be ArrayBuffer`,
|
||||
};
|
||||
default: {
|
||||
const ctor = binaryTypes[expected];
|
||||
if (!ctor) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected ${expected} to be a binary type`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pass: actual instanceof ctor,
|
||||
message: () => `Expected ${actual} to be ${expected}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
toRun(cmds: string[], optionalStdout?: string, expectedCode: number = 0) {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), ...cmds],
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "inherit"],
|
||||
});
|
||||
|
||||
if (result.exitCode !== expectedCode) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Command ${cmds.join(" ")} failed:` + "\n" + result.stdout.toString("utf-8"),
|
||||
};
|
||||
}
|
||||
|
||||
if (optionalStdout != null) {
|
||||
return {
|
||||
pass: result.stdout.toString("utf-8") === optionalStdout,
|
||||
message: () =>
|
||||
`Expected ${cmds.join(" ")} to output ${optionalStdout} but got ${result.stdout.toString("utf-8")}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pass: true,
|
||||
message: () => `Expected ${cmds.join(" ")} to fail`,
|
||||
};
|
||||
},
|
||||
toThrowWithCode(fn: CallableFunction, cls: CallableFunction, code: string) {
|
||||
try {
|
||||
fn();
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Received function did not throw`,
|
||||
};
|
||||
} catch (e) {
|
||||
// expect(e).toBeInstanceOf(cls);
|
||||
if (!(e instanceof cls)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected ${expected} to be a binary type`,
|
||||
message: () => `Expected error to be instanceof ${cls.name}; got ${e.__proto__.constructor.name}`,
|
||||
};
|
||||
}
|
||||
|
||||
// expect(e).toHaveProperty("code");
|
||||
if (!("code" in e)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected error to have property 'code'; got ${e}`,
|
||||
};
|
||||
}
|
||||
|
||||
// expect(e.code).toEqual(code);
|
||||
if (e.code !== code) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected error to have code '${code}'; got ${e.code}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pass: actual instanceof ctor,
|
||||
message: () => `Expected ${actual} to be ${expected}`,
|
||||
pass: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
toRun(cmds: string[], optionalStdout?: string, expectedCode: number = 0) {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), ...cmds],
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "inherit"],
|
||||
});
|
||||
|
||||
if (result.exitCode !== expectedCode) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Command ${cmds.join(" ")} failed:` + "\n" + result.stdout.toString("utf-8"),
|
||||
};
|
||||
}
|
||||
|
||||
if (optionalStdout != null) {
|
||||
return {
|
||||
pass: result.stdout.toString("utf-8") === optionalStdout,
|
||||
message: () =>
|
||||
`Expected ${cmds.join(" ")} to output ${optionalStdout} but got ${result.stdout.toString("utf-8")}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pass: true,
|
||||
message: () => `Expected ${cmds.join(" ")} to fail`,
|
||||
};
|
||||
},
|
||||
toThrowWithCode(fn: CallableFunction, cls: CallableFunction, code: string) {
|
||||
try {
|
||||
fn();
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Received function did not throw`,
|
||||
};
|
||||
} catch (e) {
|
||||
// expect(e).toBeInstanceOf(cls);
|
||||
if (!(e instanceof cls)) {
|
||||
},
|
||||
async toThrowWithCodeAsync(fn: CallableFunction, cls: CallableFunction, code: string) {
|
||||
try {
|
||||
await fn();
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected error to be instanceof ${cls.name}; got ${e.__proto__.constructor.name}`,
|
||||
message: () => `Received function did not throw`,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// expect(e).toBeInstanceOf(cls);
|
||||
if (!(e instanceof cls)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected error to be instanceof ${cls.name}; got ${e.__proto__.constructor.name}`,
|
||||
};
|
||||
}
|
||||
|
||||
// expect(e).toHaveProperty("code");
|
||||
if (!("code" in e)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected error to have property 'code'; got ${e}`,
|
||||
};
|
||||
}
|
||||
|
||||
// expect(e.code).toEqual(code);
|
||||
if (e.code !== code) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected error to have code '${code}'; got ${e.code}`,
|
||||
};
|
||||
}
|
||||
|
||||
// expect(e).toHaveProperty("code");
|
||||
if (!("code" in e)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected error to have property 'code'; got ${e}`,
|
||||
pass: true,
|
||||
};
|
||||
}
|
||||
|
||||
// expect(e.code).toEqual(code);
|
||||
if (e.code !== code) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected error to have code '${code}'; got ${e.code}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pass: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
async toThrowWithCodeAsync(fn: CallableFunction, cls: CallableFunction, code: string) {
|
||||
try {
|
||||
await fn();
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Received function did not throw`,
|
||||
};
|
||||
} catch (e) {
|
||||
// expect(e).toBeInstanceOf(cls);
|
||||
if (!(e instanceof cls)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected error to be instanceof ${cls.name}; got ${e.__proto__.constructor.name}`,
|
||||
};
|
||||
}
|
||||
|
||||
// expect(e).toHaveProperty("code");
|
||||
if (!("code" in e)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected error to have property 'code'; got ${e}`,
|
||||
};
|
||||
}
|
||||
|
||||
// expect(e.code).toEqual(code);
|
||||
if (e.code !== code) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected error to have code '${code}'; got ${e.code}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pass: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export function ospath(path: string) {
|
||||
if (isWindows) {
|
||||
@@ -1115,34 +1115,35 @@ String.prototype.isUTF16 = function () {
|
||||
return require("bun:internal-for-testing").jscInternals.isUTF16String(this);
|
||||
};
|
||||
|
||||
expect.extend({
|
||||
toBeLatin1String(actual: unknown) {
|
||||
if ((actual as string).isLatin1()) {
|
||||
if (expect.extend)
|
||||
expect.extend({
|
||||
toBeLatin1String(actual: unknown) {
|
||||
if ((actual as string).isLatin1()) {
|
||||
return {
|
||||
pass: true,
|
||||
message: () => `Expected ${actual} to be a Latin1 string`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pass: true,
|
||||
pass: false,
|
||||
message: () => `Expected ${actual} to be a Latin1 string`,
|
||||
};
|
||||
}
|
||||
},
|
||||
toBeUTF16String(actual: unknown) {
|
||||
if ((actual as string).isUTF16()) {
|
||||
return {
|
||||
pass: true,
|
||||
message: () => `Expected ${actual} to be a UTF16 string`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected ${actual} to be a Latin1 string`,
|
||||
};
|
||||
},
|
||||
toBeUTF16String(actual: unknown) {
|
||||
if ((actual as string).isUTF16()) {
|
||||
return {
|
||||
pass: true,
|
||||
pass: false,
|
||||
message: () => `Expected ${actual} to be a UTF16 string`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected ${actual} to be a UTF16 string`,
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
interface BunHarnessTestMatchers {
|
||||
toBeLatin1String(): void;
|
||||
|
||||
7
test/js/bun/http/hspec.test.ts
Normal file
7
test/js/bun/http/hspec.test.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { runTests } from "./http-spec.ts";
|
||||
|
||||
test("https://github.com/uNetworking/h1spec tests pass", async () => {
|
||||
const passed = await runTests();
|
||||
expect(passed).toBe(true);
|
||||
});
|
||||
344
test/js/bun/http/http-spec.ts
Normal file
344
test/js/bun/http/http-spec.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
// https://github.com/uNetworking/h1spec
|
||||
// https://github.com/oven-sh/bun/issues/14826
|
||||
// Thanks to Alex Hultman
|
||||
import net from "net";
|
||||
|
||||
// Define test cases
|
||||
interface TestCase {
|
||||
request: string;
|
||||
description: string;
|
||||
expectedStatus: [number, number][];
|
||||
expectedTimeout?: boolean;
|
||||
}
|
||||
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
request: "G",
|
||||
description: "Fragmented method",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET ",
|
||||
description: "Fragmented URL 1",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello",
|
||||
description: "Fragmented URL 2",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello ",
|
||||
description: "Fragmented URL 3",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello HTTP",
|
||||
description: "Fragmented HTTP version",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello HTTP/1.1",
|
||||
description: "Fragmented request line",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello HTTP/1.1\r",
|
||||
description: "Fragmented request line newline 1",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello HTTP/1.1\r\n",
|
||||
description: "Fragmented request line newline 2",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello HTTP/1.1\r\nHos",
|
||||
description: "Fragmented field name",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello HTTP/1.1\r\nHost:",
|
||||
description: "Fragmented field value 1",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello HTTP/1.1\r\nHost: ",
|
||||
description: "Fragmented field value 2",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello HTTP/1.1\r\nHost: localhost",
|
||||
description: "Fragmented field value 3",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello HTTP/1.1\r\nHost: localhost\r",
|
||||
description: "Fragmented field value 4",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello HTTP/1.1\r\nHost: localhost\r\n",
|
||||
description: "Fragmented request",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET /hello HTTP/1.1\r\nHost: localhost\r\n\r",
|
||||
description: "Fragmented request termination",
|
||||
expectedStatus: [[-1, -1]],
|
||||
expectedTimeout: true,
|
||||
},
|
||||
{
|
||||
request: "GET / \r\n\r\n",
|
||||
description: "Request without HTTP version",
|
||||
expectedStatus: [[400, 599]],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.1\r\nHost: example.com\r\nExpect: 100-continue\r\n\r\n",
|
||||
description: "Request with Expect header",
|
||||
expectedStatus: [
|
||||
[100, 100],
|
||||
[200, 299],
|
||||
],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
|
||||
description: "Valid GET request",
|
||||
expectedStatus: [[200, 299]],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n",
|
||||
description: "Valid GET request with HTTP/1.0",
|
||||
expectedStatus: [[200, 299]],
|
||||
},
|
||||
{
|
||||
request: "GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
|
||||
description: "Valid GET request for a proxy URL",
|
||||
expectedStatus: [[200, 299]],
|
||||
},
|
||||
{
|
||||
request: "GET https://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
|
||||
description: "Valid GET request for an https proxy URL",
|
||||
expectedStatus: [[200, 299]],
|
||||
},
|
||||
{
|
||||
request: "GET HTTPS://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
|
||||
description: "Valid GET request for an HTTPS proxy URL",
|
||||
expectedStatus: [[200, 299]],
|
||||
},
|
||||
{
|
||||
request: "GET HTTPZ://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
|
||||
description: "Invalid GET request for an HTTPS proxy URL",
|
||||
expectedStatus: [[400, 499]],
|
||||
},
|
||||
{
|
||||
request: "GET H-TTP://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
|
||||
description: "Invalid GET request for an HTTPS proxy URL",
|
||||
expectedStatus: [[400, 499]],
|
||||
},
|
||||
{
|
||||
request: "GET HTTP://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
|
||||
description: "Valid GET request for an HTTP proxy URL",
|
||||
expectedStatus: [[200, 299]],
|
||||
},
|
||||
{
|
||||
request: "GET HTTP/1.1\r\nHost: example.com\r\n\r\n",
|
||||
description: "Invalid GET request target (space)",
|
||||
expectedStatus: [[400, 499]],
|
||||
},
|
||||
{
|
||||
request: "GET ^ HTTP/1.1\r\nHost: example.com\r\n\r\n",
|
||||
description: "Invalid GET request target (caret)",
|
||||
expectedStatus: [[400, 499]],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.1\r\nhoSt:\texample.com\r\nempty:\r\n\r\n",
|
||||
description: "Valid GET request with edge cases",
|
||||
expectedStatus: [[200, 299]],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.1\r\nHost: example.com\r\nX-Invalid[]: test\r\n\r\n",
|
||||
description: "Invalid header characters",
|
||||
expectedStatus: [[400, 499]],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.1\r\nContent-Length: 5\r\n\r\n",
|
||||
description: "Missing Host header",
|
||||
expectedStatus: [[400, 499]],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: -123456789123456789123456789\r\n\r\n",
|
||||
description: "Overflowing negative Content-Length header",
|
||||
expectedStatus: [[400, 499]],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: -1234\r\n\r\n",
|
||||
description: "Negative Content-Length header",
|
||||
expectedStatus: [[400, 499]],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: abc\r\n\r\n",
|
||||
description: "Non-numeric Content-Length header",
|
||||
expectedStatus: [[400, 499]],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.1\r\nHost: example.com\r\nX-Empty-Header: \r\n\r\n",
|
||||
description: "Empty header value",
|
||||
expectedStatus: [[200, 299]],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.1\r\nHost: example.com\r\nX-Bad-Control-Char: test\x07\r\n\r\n",
|
||||
description: "Header containing invalid control character",
|
||||
expectedStatus: [[400, 499]],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/9.9\r\nHost: example.com\r\n\r\n",
|
||||
description: "Invalid HTTP version",
|
||||
expectedStatus: [
|
||||
[400, 499],
|
||||
[500, 599],
|
||||
],
|
||||
},
|
||||
{
|
||||
request: "Extra lineGET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
|
||||
description: "Invalid prefix of request",
|
||||
expectedStatus: [
|
||||
[400, 499],
|
||||
[500, 599],
|
||||
],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.1\r\nHost: example.com\r\n\rSome-Header: Test\r\n\r\n",
|
||||
description: "Invalid line ending",
|
||||
expectedStatus: [[400, 499]],
|
||||
},
|
||||
{
|
||||
request: "POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello",
|
||||
description: "Valid POST request with body",
|
||||
expectedStatus: [
|
||||
[200, 299],
|
||||
[404, 404],
|
||||
],
|
||||
},
|
||||
{
|
||||
request: "GET / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n",
|
||||
description: "Conflicting Transfer-Encoding and Content-Length",
|
||||
expectedStatus: [[400, 499]],
|
||||
},
|
||||
];
|
||||
|
||||
export async function runTestsStandalone(host: string, port: number) {
|
||||
const results = await Promise.all(testCases.map(testCase => runTestCase(testCase, host, parseInt(port, 10))));
|
||||
|
||||
const passedCount = results.filter(result => result).length;
|
||||
console.log(`\n${passedCount} out of ${testCases.length} tests passed.`);
|
||||
return passedCount === testCases.length;
|
||||
}
|
||||
|
||||
// Run all test cases in parallel
|
||||
export async function runTests() {
|
||||
let host, port;
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
return new Response("Hello, world!");
|
||||
},
|
||||
});
|
||||
|
||||
host = server.url.hostname;
|
||||
port = server.url.port;
|
||||
return await runTestsStandalone(host, port);
|
||||
}
|
||||
|
||||
// Run a single test case with a 3-second timeout on reading
|
||||
async function runTestCase(testCase: TestCase, host: string, port: number): Promise<boolean> {
|
||||
try {
|
||||
const conn = new Promise((resolve, reject) => {
|
||||
const client = net.createConnection({ host, port }, () => {
|
||||
resolve(client);
|
||||
});
|
||||
client.on("error", reject);
|
||||
});
|
||||
|
||||
const client: net.Socket = await conn;
|
||||
|
||||
// Send the request
|
||||
client.write(Buffer.from(testCase.request));
|
||||
|
||||
// Set up a read timeout promise
|
||||
const readTimeout = new Promise<boolean>(resolve => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (testCase.expectedTimeout) {
|
||||
console.log(`✅ ${testCase.description}: Server waited successfully`);
|
||||
client.destroy(); // Ensure the connection is closed on timeout
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error(`❌ ${testCase.description}: Read operation timed out`);
|
||||
client.destroy(); // Ensure the connection is closed on timeout
|
||||
resolve(false);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
client.on("data", data => {
|
||||
// Clear the timeout if read completes
|
||||
clearTimeout(timeoutId);
|
||||
const response = data.toString();
|
||||
const statusCode = parseStatusCode(response);
|
||||
|
||||
const isSuccess = testCase.expectedStatus.some(([min, max]) => statusCode >= min && statusCode <= max);
|
||||
if (!isSuccess) {
|
||||
console.log(JSON.stringify(response, null, 2));
|
||||
}
|
||||
console.log(
|
||||
`${isSuccess ? "✅" : "❌"} ${
|
||||
testCase.description
|
||||
}: Response Status Code ${statusCode}, Expected ranges: ${JSON.stringify(testCase.expectedStatus)}`,
|
||||
);
|
||||
client.destroy();
|
||||
resolve(isSuccess);
|
||||
});
|
||||
|
||||
client.on("error", error => {
|
||||
clearTimeout(timeoutId);
|
||||
console.error(`Error in test "${testCase.description}":`, error);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for the read operation or timeout
|
||||
return await readTimeout;
|
||||
} catch (error) {
|
||||
console.error(`Error in test "${testCase.description}":`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the HTTP status code from the response
|
||||
function parseStatusCode(response: string): number {
|
||||
const statusLine = response.split("\r\n")[0];
|
||||
const match = statusLine.match(/HTTP\/1\.\d (\d{3})/);
|
||||
return match ? parseInt(match[1], 10) : 0;
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
if (process.argv.length > 2) {
|
||||
await runTestsStandalone(process.argv[2], parseInt(process.argv[3], 10));
|
||||
} else {
|
||||
await runTests();
|
||||
}
|
||||
}
|
||||
@@ -2073,3 +2073,75 @@ it("allow custom timeout per request", async () => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text()).resolves.toBe("Hello, World!");
|
||||
}, 20_000);
|
||||
|
||||
it("#6462", async () => {
|
||||
let headers: string[] = [];
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
async fetch(request) {
|
||||
for (const key of request.headers.keys()) {
|
||||
headers = headers.concat([[key, request.headers.get(key)]]);
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
"headers": headers,
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const bytes = Buffer.from(`GET / HTTP/1.1\r\nConnection: close\r\nHost: ${server.hostname}\r\nTest!: test\r\n\r\n`);
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
await Bun.connect({
|
||||
port: server.port,
|
||||
hostname: server.hostname,
|
||||
socket: {
|
||||
open(socket) {
|
||||
const wrote = socket.write(bytes);
|
||||
console.log("wrote", wrote);
|
||||
},
|
||||
data(socket, data) {
|
||||
console.log(data.toString("utf8"));
|
||||
},
|
||||
close(socket) {
|
||||
resolve();
|
||||
},
|
||||
},
|
||||
});
|
||||
await promise;
|
||||
|
||||
expect(headers).toStrictEqual([
|
||||
["connection", "close"],
|
||||
["host", "localhost"],
|
||||
["test!", "test"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("#6583", async () => {
|
||||
const callback = mock();
|
||||
using server = Bun.serve({
|
||||
fetch: callback,
|
||||
port: 0,
|
||||
hostname: "localhost",
|
||||
});
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
await Bun.connect({
|
||||
port: server.port,
|
||||
hostname: server.hostname,
|
||||
tls: true,
|
||||
socket: {
|
||||
open(socket) {
|
||||
socket.write("GET / HTTP/1.1\r\nConnection: close\r\nHost: localhost\r\n\r\n");
|
||||
},
|
||||
data(socket, data) {
|
||||
console.log(data.toString("utf8"));
|
||||
},
|
||||
close(socket) {
|
||||
resolve();
|
||||
},
|
||||
},
|
||||
});
|
||||
await promise;
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
18
test/js/node/http/max-header-size-fixture.ts
generated
18
test/js/node/http/max-header-size-fixture.ts
generated
@@ -1,6 +1,6 @@
|
||||
import http from "node:http";
|
||||
|
||||
if (http.maxHeaderSize !== parseInt(process.env.BUN_HTTP_MAX_HEADER_SIZE, 10)) {
|
||||
if (http.maxHeaderSize !== parseInt(process.env.BUN_HTTP_MAX_HEADER_SIZE ?? "0", 10)) {
|
||||
throw new Error("BUN_HTTP_MAX_HEADER_SIZE is not set to the correct value");
|
||||
}
|
||||
|
||||
@@ -18,16 +18,20 @@ await fetch(`${server.url}/`, {
|
||||
});
|
||||
|
||||
try {
|
||||
await fetch(`${server.url}/`, {
|
||||
const response = await fetch(`${server.url}/`, {
|
||||
headers: {
|
||||
"Huge": Buffer.alloc(http.maxHeaderSize + 1024, "abc").toString(),
|
||||
},
|
||||
});
|
||||
throw new Error("bad");
|
||||
} catch (e) {
|
||||
if (e.message.includes("bad")) {
|
||||
process.exit(1);
|
||||
if (response.status === 431) {
|
||||
throw new Error("good!!");
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
throw new Error("bad!");
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.includes("good!!")) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -18,22 +18,23 @@ test("maxHeaderSize", async () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
async () =>
|
||||
await fetch(`${server.url}/`, {
|
||||
headers: {
|
||||
"Huge": Buffer.alloc(8 * 1024, "abc").toString(),
|
||||
},
|
||||
}),
|
||||
).toThrow();
|
||||
expect(
|
||||
async () =>
|
||||
await fetch(`${server.url}/`, {
|
||||
headers: {
|
||||
"Huge": Buffer.alloc(512, "abc").toString(),
|
||||
},
|
||||
}),
|
||||
).not.toThrow();
|
||||
{
|
||||
const response = await fetch(`${server.url}/`, {
|
||||
headers: {
|
||||
"Huge": Buffer.alloc(8 * 1024, "abc").toString(),
|
||||
},
|
||||
});
|
||||
expect(response.status).toBe(431);
|
||||
}
|
||||
|
||||
{
|
||||
const response = await fetch(`${server.url}/`, {
|
||||
headers: {
|
||||
"Huge": Buffer.alloc(15 * 1024, "abc").toString(),
|
||||
},
|
||||
});
|
||||
expect(response.status).toBe(431);
|
||||
}
|
||||
}
|
||||
http.maxHeaderSize = 16 * 1024;
|
||||
{
|
||||
@@ -45,22 +46,23 @@ test("maxHeaderSize", async () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
async () =>
|
||||
await fetch(`${server.url}/`, {
|
||||
headers: {
|
||||
"Huge": Buffer.alloc(15 * 1024, "abc").toString(),
|
||||
},
|
||||
}),
|
||||
).not.toThrow();
|
||||
expect(
|
||||
async () =>
|
||||
await fetch(`${server.url}/`, {
|
||||
headers: {
|
||||
"Huge": Buffer.alloc(17 * 1024, "abc").toString(),
|
||||
},
|
||||
}),
|
||||
).toThrow();
|
||||
{
|
||||
const response = await fetch(`${server.url}/`, {
|
||||
headers: {
|
||||
"Huge": Buffer.alloc(15 * 1024, "abc").toString(),
|
||||
},
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
}
|
||||
|
||||
{
|
||||
const response = await fetch(`${server.url}/`, {
|
||||
headers: {
|
||||
"Huge": Buffer.alloc(17 * 1024, "abc").toString(),
|
||||
},
|
||||
});
|
||||
expect(response.status).toBe(431);
|
||||
}
|
||||
}
|
||||
|
||||
http.maxHeaderSize = originalMaxHeaderSize;
|
||||
|
||||
79
test/js/node/http/node-http-proxy.js
Normal file
79
test/js/node/http/node-http-proxy.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import assert from "node:assert";
|
||||
import { createServer, request } from "node:http";
|
||||
import url from "node:url";
|
||||
|
||||
export async function run() {
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
|
||||
const proxyServer = createServer(function (req, res) {
|
||||
// Use URL object instead of deprecated url.parse
|
||||
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
|
||||
|
||||
const options = {
|
||||
protocol: parsedUrl.protocol,
|
||||
hostname: parsedUrl.hostname,
|
||||
port: parsedUrl.port,
|
||||
path: parsedUrl.pathname + parsedUrl.search,
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
};
|
||||
|
||||
const proxyRequest = request(options, function (proxyResponse) {
|
||||
res.writeHead(proxyResponse.statusCode, proxyResponse.headers);
|
||||
proxyResponse.pipe(res); // Use pipe instead of manual data handling
|
||||
});
|
||||
|
||||
proxyRequest.on("error", error => {
|
||||
console.error("Proxy Request Error:", error);
|
||||
res.writeHead(500);
|
||||
res.end("Proxy Error");
|
||||
});
|
||||
|
||||
req.pipe(proxyRequest); // Use pipe instead of manual data handling
|
||||
});
|
||||
|
||||
proxyServer.listen(0, "localhost", async () => {
|
||||
const address = proxyServer.address();
|
||||
|
||||
const options = {
|
||||
protocol: "http:",
|
||||
hostname: "localhost",
|
||||
port: address.port,
|
||||
path: "/", // Change path to /
|
||||
headers: {
|
||||
Host: "example.com",
|
||||
"accept-encoding": "identity",
|
||||
},
|
||||
};
|
||||
|
||||
const req = request(options, res => {
|
||||
let data = "";
|
||||
res.on("data", chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on("end", () => {
|
||||
try {
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
assert(data.length > 0);
|
||||
assert(data.includes("This domain is for use in illustrative examples in documents"));
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", err => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
|
||||
await promise;
|
||||
proxyServer.close();
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
run().catch(console.error);
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
// @ts-nocheck
|
||||
import { bunExe } from "bun:harness";
|
||||
import { bunEnv, randomPort } from "harness";
|
||||
/**
|
||||
* All new tests in this file should also run in Node.js.
|
||||
*
|
||||
* Do not add any tests that only run in Bun.
|
||||
*
|
||||
* A handful of older tests do not run in Node in this file. These tests should be updated to run in Node, or deleted.
|
||||
*/
|
||||
import { bunEnv, randomPort, bunExe } from "harness";
|
||||
import { createTest } from "node-harness";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { EventEmitter, once } from "node:events";
|
||||
@@ -23,10 +28,9 @@ import { tmpdir } from "node:os";
|
||||
import * as path from "node:path";
|
||||
import * as stream from "node:stream";
|
||||
import { PassThrough } from "node:stream";
|
||||
import url from "node:url";
|
||||
import * as zlib from "node:zlib";
|
||||
import { run as runHTTPProxyTest } from "./node-http-proxy.js";
|
||||
const { describe, expect, it, beforeAll, afterAll, createDoneDotAll, mock, test } = createTest(import.meta.path);
|
||||
|
||||
function listen(server: Server, protocol: string = "http"): Promise<URL> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject("Timed out"), 5000).unref();
|
||||
@@ -772,62 +776,8 @@ describe("node:http", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("request via http proxy, issue#4295", done => {
|
||||
const proxyServer = createServer(function (req, res) {
|
||||
let option = url.parse(req.url);
|
||||
option.host = req.headers.host;
|
||||
option.headers = req.headers;
|
||||
|
||||
const proxyRequest = request(option, function (proxyResponse) {
|
||||
res.writeHead(proxyResponse.statusCode, proxyResponse.headers);
|
||||
proxyResponse.on("data", function (chunk) {
|
||||
res.write(chunk, "binary");
|
||||
});
|
||||
proxyResponse.on("end", function () {
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
req.on("data", function (chunk) {
|
||||
proxyRequest.write(chunk, "binary");
|
||||
});
|
||||
req.on("end", function () {
|
||||
proxyRequest.end();
|
||||
});
|
||||
});
|
||||
|
||||
proxyServer.listen({ port: 0 }, async (_err, hostname, port) => {
|
||||
const options = {
|
||||
protocol: "http:",
|
||||
hostname: hostname,
|
||||
port: port,
|
||||
path: "http://example.com",
|
||||
headers: {
|
||||
Host: "example.com",
|
||||
"accept-encoding": "identity",
|
||||
},
|
||||
};
|
||||
|
||||
const req = request(options, res => {
|
||||
let data = "";
|
||||
res.on("data", chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on("end", () => {
|
||||
try {
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(data.length).toBeGreaterThan(0);
|
||||
expect(data).toContain("This domain is for use in illustrative examples in documents");
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on("error", err => {
|
||||
done(err);
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
it("request via http proxy, issue#4295", async () => {
|
||||
await runHTTPProxyTest();
|
||||
});
|
||||
|
||||
it("should correctly stream a multi-chunk response #5320", async done => {
|
||||
|
||||
@@ -16,4 +16,4 @@ for (let key in harness.bunEnv) {
|
||||
process.env[key] = harness.bunEnv[key] + "";
|
||||
}
|
||||
|
||||
Bun.$.env(process.env);
|
||||
if (Bun.$?.env) Bun.$.env(process.env);
|
||||
|
||||
Reference in New Issue
Block a user