mirror of
https://github.com/oven-sh/bun
synced 2026-02-21 00:02:19 +00:00
Compare commits
1 Commits
claude/fix
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92693b40ee |
@@ -22,6 +22,7 @@
|
||||
#include "BunClientData.h"
|
||||
#include "CallSite.h"
|
||||
#include "ErrorStackTrace.h"
|
||||
#include "JSDOMException.h"
|
||||
#include "headers-handwritten.h"
|
||||
|
||||
using namespace JSC;
|
||||
@@ -379,6 +380,9 @@ static String computeErrorInfoWithoutPrepareStackTrace(
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
message = instance->sanitizedMessageString(lexicalGlobalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
} else if (auto* domException = jsDynamicCast<WebCore::JSDOMException*>(errorInstance)) {
|
||||
name = domException->wrapped().name();
|
||||
message = domException->wrapped().message();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,11 +57,10 @@ static EncodedJSValue assignHeadersFromFetchHeaders(FetchHeaders& impl, JSObject
|
||||
for (const auto& it : vec) {
|
||||
const auto& name = it.key;
|
||||
const auto& value = it.value;
|
||||
const auto lowercaseName = WTF::httpHeaderNameStringImpl(name);
|
||||
const auto canonicalName = WTF::httpHeaderNameDefaultCaseStringImpl(name);
|
||||
const auto impl = WTF::httpHeaderNameStringImpl(name);
|
||||
JSString* jsValue = jsString(vm, value);
|
||||
obj->putDirect(vm, Identifier::fromString(vm, lowercaseName), jsValue, 0);
|
||||
array->putDirectIndex(globalObject, arrayI++, jsString(vm, canonicalName));
|
||||
obj->putDirect(vm, Identifier::fromString(vm, impl), jsValue, 0);
|
||||
array->putDirectIndex(globalObject, arrayI++, jsString(vm, impl));
|
||||
array->putDirectIndex(globalObject, arrayI++, jsValue);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
}
|
||||
@@ -75,10 +74,9 @@ static EncodedJSValue assignHeadersFromFetchHeaders(FetchHeaders& impl, JSObject
|
||||
if (count > 0) {
|
||||
JSC::JSArray* setCookies = constructEmptyArray(globalObject, nullptr, count);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
const auto setCookieHeaderStringLowercase = WTF::httpHeaderNameStringImpl(HTTPHeaderName::SetCookie);
|
||||
const auto setCookieHeaderStringCanonical = WTF::httpHeaderNameDefaultCaseStringImpl(HTTPHeaderName::SetCookie);
|
||||
const auto setCookieHeaderString = WTF::httpHeaderNameStringImpl(HTTPHeaderName::SetCookie);
|
||||
|
||||
JSString* setCookie = jsString(vm, setCookieHeaderStringCanonical);
|
||||
JSString* setCookie = jsString(vm, setCookieHeaderString);
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
auto* out = jsString(vm, values[i]);
|
||||
@@ -89,7 +87,7 @@ static EncodedJSValue assignHeadersFromFetchHeaders(FetchHeaders& impl, JSObject
|
||||
}
|
||||
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
obj->putDirect(vm, JSC::Identifier::fromString(vm, setCookieHeaderStringLowercase), setCookies, 0);
|
||||
obj->putDirect(vm, JSC::Identifier::fromString(vm, setCookieHeaderString), setCookies, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,14 +154,14 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, JSVal
|
||||
|
||||
HTTPHeaderIdentifiers& identifiers = WebCore::clientData(vm)->httpHeaderIdentifiers();
|
||||
Identifier nameIdentifier;
|
||||
JSString* rawHeaderNameString = nullptr;
|
||||
JSString* nameString = nullptr;
|
||||
|
||||
if (WebCore::findHTTPHeaderName(nameView, name)) {
|
||||
rawHeaderNameString = jsString(vm, WTF::httpHeaderNameDefaultCaseStringImpl(name));
|
||||
nameString = identifiers.stringFor(globalObject, name);
|
||||
nameIdentifier = identifiers.identifierFor(vm, name);
|
||||
} else {
|
||||
WTF::String wtfString = nameView.toString();
|
||||
rawHeaderNameString = jsString(vm, wtfString);
|
||||
nameString = jsString(vm, wtfString);
|
||||
nameIdentifier = Identifier::fromString(vm, wtfString.convertToASCIILowercase());
|
||||
}
|
||||
|
||||
@@ -171,7 +169,7 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, JSVal
|
||||
if (!setCookiesHeaderArray) {
|
||||
setCookiesHeaderArray = constructEmptyArray(globalObject, nullptr);
|
||||
RETURN_IF_EXCEPTION(scope, );
|
||||
setCookiesHeaderString = rawHeaderNameString;
|
||||
setCookiesHeaderString = nameString;
|
||||
headersObject->putDirect(vm, nameIdentifier, setCookiesHeaderArray, 0);
|
||||
RETURN_IF_EXCEPTION(scope, void());
|
||||
}
|
||||
@@ -183,7 +181,7 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, JSVal
|
||||
} else {
|
||||
headersObject->putDirectMayBeIndex(globalObject, nameIdentifier, jsValue);
|
||||
RETURN_IF_EXCEPTION(scope, void());
|
||||
arrayValues.append(rawHeaderNameString);
|
||||
arrayValues.append(nameString);
|
||||
arrayValues.append(jsValue);
|
||||
RETURN_IF_EXCEPTION(scope, void());
|
||||
}
|
||||
@@ -337,15 +335,15 @@ static EncodedJSValue assignHeadersFromUWebSockets(uWS::HttpRequest* request, JS
|
||||
memcpy(data.data(), pair.second.data(), pair.second.length());
|
||||
|
||||
HTTPHeaderName name;
|
||||
WTF::String nameString;
|
||||
WTF::String lowercasedNameString;
|
||||
WTF::String canonicalNameString;
|
||||
|
||||
if (WebCore::findHTTPHeaderName(nameView, name)) {
|
||||
lowercasedNameString = WTF::httpHeaderNameStringImpl(name);
|
||||
canonicalNameString = WTF::httpHeaderNameDefaultCaseStringImpl(name);
|
||||
nameString = WTF::httpHeaderNameStringImpl(name);
|
||||
lowercasedNameString = nameString;
|
||||
} else {
|
||||
canonicalNameString = nameView.toString();
|
||||
lowercasedNameString = canonicalNameString.convertToASCIILowercase();
|
||||
nameString = nameView.toString();
|
||||
lowercasedNameString = nameString.convertToASCIILowercase();
|
||||
}
|
||||
|
||||
JSString* jsValue = jsString(vm, value);
|
||||
@@ -354,7 +352,7 @@ static EncodedJSValue assignHeadersFromUWebSockets(uWS::HttpRequest* request, JS
|
||||
if (!setCookiesHeaderArray) {
|
||||
setCookiesHeaderArray = constructEmptyArray(globalObject, nullptr);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
setCookiesHeaderString = jsString(vm, canonicalNameString);
|
||||
setCookiesHeaderString = jsString(vm, nameString);
|
||||
headersObject->putDirect(vm, Identifier::fromString(vm, lowercasedNameString), setCookiesHeaderArray, 0);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
}
|
||||
@@ -365,7 +363,7 @@ static EncodedJSValue assignHeadersFromUWebSockets(uWS::HttpRequest* request, JS
|
||||
|
||||
} else {
|
||||
headersObject->putDirect(vm, Identifier::fromString(vm, lowercasedNameString), jsValue, 0);
|
||||
array->putDirectIndex(globalObject, i++, jsString(vm, canonicalNameString));
|
||||
array->putDirectIndex(globalObject, i++, jsString(vm, nameString));
|
||||
array->putDirectIndex(globalObject, i++, jsValue);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
}
|
||||
|
||||
@@ -46,6 +46,10 @@
|
||||
#include <wtf/PointerPreparations.h>
|
||||
#include <wtf/URL.h>
|
||||
|
||||
#include "FormatStackTraceForJS.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
#include <JavaScriptCore/Interpreter.h>
|
||||
|
||||
namespace WebCore {
|
||||
using namespace JSC;
|
||||
|
||||
@@ -120,6 +124,34 @@ static const HashTableValue JSDOMExceptionConstructorTableValues[] = {
|
||||
{ "DATA_CLONE_ERR"_s, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::ConstantInteger, NoIntrinsic, { HashTableValue::ConstantType, 25 } },
|
||||
};
|
||||
|
||||
static void captureStackTraceForDOMException(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSObject* errorObject)
|
||||
{
|
||||
if (!vm.topCallFrame)
|
||||
return;
|
||||
|
||||
auto* zigGlobalObject = jsDynamicCast<Zig::GlobalObject*>(lexicalGlobalObject);
|
||||
if (!zigGlobalObject)
|
||||
zigGlobalObject = ::defaultGlobalObject(lexicalGlobalObject);
|
||||
|
||||
size_t stackTraceLimit = zigGlobalObject->stackTraceLimit().value();
|
||||
if (stackTraceLimit == 0)
|
||||
stackTraceLimit = Bun::DEFAULT_ERROR_STACK_TRACE_LIMIT;
|
||||
|
||||
WTF::Vector<JSC::StackFrame> stackTrace;
|
||||
vm.interpreter.getStackTrace(errorObject, stackTrace, 0, stackTraceLimit);
|
||||
|
||||
if (stackTrace.isEmpty())
|
||||
return;
|
||||
|
||||
unsigned int line = 0;
|
||||
unsigned int column = 0;
|
||||
String sourceURL;
|
||||
JSValue result = Bun::computeErrorInfoWrapperToJSValue(vm, stackTrace, line, column, sourceURL, errorObject, nullptr);
|
||||
|
||||
if (result)
|
||||
errorObject->putDirect(vm, vm.propertyNames->stack, result, 0);
|
||||
}
|
||||
|
||||
template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSDOMExceptionDOMConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame)
|
||||
{
|
||||
auto& vm = JSC::getVM(lexicalGlobalObject);
|
||||
@@ -362,26 +394,12 @@ void JSDOMExceptionOwner::finalize(JSC::Handle<JSC::Unknown> handle, void* conte
|
||||
// #endif
|
||||
// #endif
|
||||
|
||||
JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject* globalObject, Ref<DOMException>&& impl)
|
||||
JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, Ref<DOMException>&& impl)
|
||||
{
|
||||
|
||||
// if constexpr (std::is_polymorphic_v<DOMException>) {
|
||||
// #if ENABLE(BINDING_INTEGRITY)
|
||||
// // const void* actualVTablePointer = getVTablePointer(impl.ptr());
|
||||
// #if PLATFORM(WIN)
|
||||
// void* expectedVTablePointer = __identifier("??_7DOMException@WebCore@@6B@");
|
||||
// #else
|
||||
// // void* expectedVTablePointer = &_ZTVN7WebCore12DOMExceptionE[2];
|
||||
// #endif
|
||||
|
||||
// // If you hit this assertion you either have a use after free bug, or
|
||||
// // DOMException has subclasses. If DOMException has subclasses that get passed
|
||||
// // to toJS() we currently require DOMException you to opt out of binding hardening
|
||||
// // by adding the SkipVTableValidation attribute to the interface IDL definition
|
||||
// // RELEASE_ASSERT(actualVTablePointer == expectedVTablePointer);
|
||||
// #endif
|
||||
// }
|
||||
return createWrapper<DOMException>(globalObject, WTF::move(impl));
|
||||
auto* wrapper = createWrapper<DOMException>(globalObject, WTF::move(impl));
|
||||
auto& vm = globalObject->vm();
|
||||
captureStackTraceForDOMException(vm, lexicalGlobalObject ? lexicalGlobalObject : globalObject, wrapper);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, DOMException& impl)
|
||||
|
||||
@@ -34,112 +34,34 @@ const { FakeSocket } = require("internal/http/FakeSocket");
|
||||
var defaultIncomingOpts = { type: "request" };
|
||||
const nop = () => {};
|
||||
|
||||
// Map of lowercase header names to their canonical (Title-Case) form.
|
||||
// Used by assignHeadersSlow to restore proper casing for rawHeaders,
|
||||
// since the Fetch API normalizes all header names to lowercase.
|
||||
const canonicalHeaderNames: Record<string, string> = {
|
||||
"accept": "Accept",
|
||||
"accept-charset": "Accept-Charset",
|
||||
"accept-encoding": "Accept-Encoding",
|
||||
"accept-language": "Accept-Language",
|
||||
"accept-ranges": "Accept-Ranges",
|
||||
"access-control-allow-credentials": "Access-Control-Allow-Credentials",
|
||||
"access-control-allow-headers": "Access-Control-Allow-Headers",
|
||||
"access-control-allow-methods": "Access-Control-Allow-Methods",
|
||||
"access-control-allow-origin": "Access-Control-Allow-Origin",
|
||||
"access-control-expose-headers": "Access-Control-Expose-Headers",
|
||||
"access-control-max-age": "Access-Control-Max-Age",
|
||||
"access-control-request-headers": "Access-Control-Request-Headers",
|
||||
"access-control-request-method": "Access-Control-Request-Method",
|
||||
"age": "Age",
|
||||
"authorization": "Authorization",
|
||||
"cache-control": "Cache-Control",
|
||||
"connection": "Connection",
|
||||
"content-disposition": "Content-Disposition",
|
||||
"content-encoding": "Content-Encoding",
|
||||
"content-language": "Content-Language",
|
||||
"content-length": "Content-Length",
|
||||
"content-location": "Content-Location",
|
||||
"content-range": "Content-Range",
|
||||
"content-security-policy": "Content-Security-Policy",
|
||||
"content-security-policy-report-only": "Content-Security-Policy-Report-Only",
|
||||
"content-type": "Content-Type",
|
||||
"cookie": "Cookie",
|
||||
"cookie2": "Cookie2",
|
||||
"date": "Date",
|
||||
"dnt": "DNT",
|
||||
"etag": "ETag",
|
||||
"expect": "Expect",
|
||||
"expires": "Expires",
|
||||
"host": "Host",
|
||||
"if-match": "If-Match",
|
||||
"if-modified-since": "If-Modified-Since",
|
||||
"if-none-match": "If-None-Match",
|
||||
"if-range": "If-Range",
|
||||
"if-unmodified-since": "If-Unmodified-Since",
|
||||
"keep-alive": "Keep-Alive",
|
||||
"last-modified": "Last-Modified",
|
||||
"link": "Link",
|
||||
"location": "Location",
|
||||
"origin": "Origin",
|
||||
"pragma": "Pragma",
|
||||
"proxy-authorization": "Proxy-Authorization",
|
||||
"range": "Range",
|
||||
"referer": "Referer",
|
||||
"referrer-policy": "Referrer-Policy",
|
||||
"refresh": "Refresh",
|
||||
"sec-fetch-dest": "Sec-Fetch-Dest",
|
||||
"sec-fetch-mode": "Sec-Fetch-Mode",
|
||||
"sec-websocket-accept": "Sec-WebSocket-Accept",
|
||||
"sec-websocket-extensions": "Sec-WebSocket-Extensions",
|
||||
"sec-websocket-key": "Sec-WebSocket-Key",
|
||||
"sec-websocket-protocol": "Sec-WebSocket-Protocol",
|
||||
"sec-websocket-version": "Sec-WebSocket-Version",
|
||||
"server-timing": "Server-Timing",
|
||||
"set-cookie": "Set-Cookie",
|
||||
"set-cookie2": "Set-Cookie2",
|
||||
"strict-transport-security": "Strict-Transport-Security",
|
||||
"te": "TE",
|
||||
"trailer": "Trailer",
|
||||
"transfer-encoding": "Transfer-Encoding",
|
||||
"upgrade": "Upgrade",
|
||||
"upgrade-insecure-requests": "Upgrade-Insecure-Requests",
|
||||
"user-agent": "User-Agent",
|
||||
"vary": "Vary",
|
||||
"via": "Via",
|
||||
"x-content-type-options": "X-Content-Type-Options",
|
||||
"x-dns-prefetch-control": "X-DNS-Prefetch-Control",
|
||||
"x-frame-options": "X-Frame-Options",
|
||||
"x-xss-protection": "X-XSS-Protection",
|
||||
};
|
||||
|
||||
function assignHeadersSlow(object, req) {
|
||||
const headers = req.headers;
|
||||
var outHeaders = Object.create(null);
|
||||
const rawHeaders: string[] = [];
|
||||
var i = 0;
|
||||
for (let key in headers) {
|
||||
var value = headers[key];
|
||||
var lowercaseKey = key.toLowerCase();
|
||||
var rawHeaderName = canonicalHeaderNames[lowercaseKey] || key;
|
||||
var originalKey = key;
|
||||
var value = headers[originalKey];
|
||||
|
||||
if (lowercaseKey !== "set-cookie") {
|
||||
key = key.toLowerCase();
|
||||
|
||||
if (key !== "set-cookie") {
|
||||
value = String(value);
|
||||
$putByValDirect(rawHeaders, i++, rawHeaderName);
|
||||
$putByValDirect(rawHeaders, i++, originalKey);
|
||||
$putByValDirect(rawHeaders, i++, value);
|
||||
outHeaders[lowercaseKey] = value;
|
||||
outHeaders[key] = value;
|
||||
} else {
|
||||
if ($isJSArray(value)) {
|
||||
outHeaders[lowercaseKey] = value.slice();
|
||||
outHeaders[key] = value.slice();
|
||||
|
||||
for (let entry of value) {
|
||||
$putByValDirect(rawHeaders, i++, rawHeaderName);
|
||||
$putByValDirect(rawHeaders, i++, originalKey);
|
||||
$putByValDirect(rawHeaders, i++, entry);
|
||||
}
|
||||
} else {
|
||||
value = String(value);
|
||||
outHeaders[lowercaseKey] = [value];
|
||||
$putByValDirect(rawHeaders, i++, rawHeaderName);
|
||||
outHeaders[key] = [value];
|
||||
$putByValDirect(rawHeaders, i++, originalKey);
|
||||
$putByValDirect(rawHeaders, i++, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,7 @@ describe("DOMException in Node.js environment", () => {
|
||||
expect(DOMException.DATA_CLONE_ERR).toBe(25);
|
||||
});
|
||||
|
||||
// TODO: missing stack trace on DOMException
|
||||
it.failing("inherits prototype properties from Error", () => {
|
||||
it("inherits prototype properties from Error", () => {
|
||||
const error = new DOMException("Test error");
|
||||
expect(error.toString()).toBe("Error: Test error");
|
||||
expect(error.stack).toBeDefined();
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("AbortSignal", () => {
|
||||
function fmt(value: any) {
|
||||
const res = {};
|
||||
for (const key in value) {
|
||||
if (key === "column" || key === "line" || key === "sourceURL") continue;
|
||||
if (key === "column" || key === "line" || key === "sourceURL" || key === "stack") continue;
|
||||
res[key] = value[key];
|
||||
}
|
||||
return res;
|
||||
|
||||
79
test/regression/issue/17877.test.ts
Normal file
79
test/regression/issue/17877.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
test("DOMException from new DOMException() has a stack trace", () => {
|
||||
const e = new DOMException("test error", "AbortError");
|
||||
expect(typeof e.stack).toBe("string");
|
||||
expect(e.stack).toContain("AbortError: test error");
|
||||
expect(e.stack).toContain("17877.test");
|
||||
expect(e instanceof DOMException).toBe(true);
|
||||
expect(e instanceof Error).toBe(true);
|
||||
});
|
||||
|
||||
test("DOMException from AbortSignal.abort() has a stack trace", () => {
|
||||
const signal = AbortSignal.abort();
|
||||
try {
|
||||
signal.throwIfAborted();
|
||||
expect.unreachable();
|
||||
} catch (err: any) {
|
||||
expect(typeof err.stack).toBe("string");
|
||||
expect(err.stack).toContain("AbortError");
|
||||
expect(err.stack).toContain("The operation was aborted");
|
||||
expect(err instanceof DOMException).toBe(true);
|
||||
expect(err instanceof Error).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("DOMException stack trace includes correct name and message", () => {
|
||||
const e = new DOMException("custom message", "NotFoundError");
|
||||
expect(typeof e.stack).toBe("string");
|
||||
expect(e.stack).toStartWith("NotFoundError: custom message\n");
|
||||
});
|
||||
|
||||
test("DOMException with default args has a stack trace", () => {
|
||||
const e = new DOMException();
|
||||
expect(typeof e.stack).toBe("string");
|
||||
expect(e.name).toBe("Error");
|
||||
expect(e.message).toBe("");
|
||||
});
|
||||
|
||||
test("DOMException stack trace shows correct call site", () => {
|
||||
function createException() {
|
||||
return new DOMException("inner", "DataError");
|
||||
}
|
||||
|
||||
const e = createException();
|
||||
expect(typeof e.stack).toBe("string");
|
||||
expect(e.stack).toContain("createException");
|
||||
});
|
||||
|
||||
test("DOMException.stack is writable", () => {
|
||||
const e = new DOMException("test", "AbortError");
|
||||
expect(typeof e.stack).toBe("string");
|
||||
e.stack = "custom stack";
|
||||
expect(e.stack).toBe("custom stack");
|
||||
});
|
||||
|
||||
test("DOMException from AbortSignal.abort() with custom reason has no stack on reason", () => {
|
||||
const reason = "custom reason string";
|
||||
const signal = AbortSignal.abort(reason);
|
||||
try {
|
||||
signal.throwIfAborted();
|
||||
expect.unreachable();
|
||||
} catch (err: any) {
|
||||
// When a custom reason (non-DOMException) is used, it's thrown as-is
|
||||
expect(err).toBe("custom reason string");
|
||||
}
|
||||
});
|
||||
|
||||
test("DOMException from AbortSignal.abort() with DOMException reason has stack", () => {
|
||||
const reason = new DOMException("custom abort", "AbortError");
|
||||
const signal = AbortSignal.abort(reason);
|
||||
try {
|
||||
signal.throwIfAborted();
|
||||
expect.unreachable();
|
||||
} catch (err: any) {
|
||||
expect(err).toBe(reason);
|
||||
expect(typeof err.stack).toBe("string");
|
||||
expect(err.stack).toContain("AbortError: custom abort");
|
||||
}
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe } from "harness";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/20433
|
||||
// req.rawHeaders on node:http server should preserve canonical header name casing
|
||||
test("node:http IncomingMessage rawHeaders should have canonical-cased header names", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`
|
||||
const http = require("node:http");
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify(req.rawHeaders));
|
||||
});
|
||||
|
||||
server.listen(0, () => {
|
||||
const port = server.address().port;
|
||||
fetch("http://localhost:" + port + "/", {
|
||||
headers: {
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept": "*/*",
|
||||
"Connection": "keep-alive",
|
||||
"Authorization": "xxx",
|
||||
"Origin": "something",
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((rawHeaders) => {
|
||||
// rawHeaders is [name, value, name, value, ...]
|
||||
const headerNames = rawHeaders.filter((_, i) => i % 2 === 0);
|
||||
console.log(JSON.stringify(headerNames));
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
`,
|
||||
],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
const headerNames: string[] = JSON.parse(stdout.trim());
|
||||
|
||||
// Known headers should be in canonical Title-Case form
|
||||
const expectedCanonical: Record<string, string> = {
|
||||
accept: "Accept",
|
||||
"accept-encoding": "Accept-Encoding",
|
||||
connection: "Connection",
|
||||
authorization: "Authorization",
|
||||
origin: "Origin",
|
||||
"content-type": "Content-Type",
|
||||
host: "Host",
|
||||
"user-agent": "User-Agent",
|
||||
};
|
||||
|
||||
for (const name of headerNames) {
|
||||
const lower = name.toLowerCase();
|
||||
if (expectedCanonical[lower]) {
|
||||
expect(name).toBe(expectedCanonical[lower]);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that multi-word headers are present and properly cased
|
||||
expect(headerNames).toContain("Accept-Encoding");
|
||||
expect(headerNames).toContain("Authorization");
|
||||
expect(headerNames).toContain("Content-Type");
|
||||
expect(headerNames).toContain("Connection");
|
||||
expect(headerNames).toContain("Origin");
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user