Compare commits

...

7 Commits

Author SHA1 Message Date
Don Isaac
7c56a70555 fix typo in cookie docs 2025-03-26 13:16:40 -07:00
Don Isaac
52d6696fad fixes 2025-03-26 13:14:41 -07:00
Don Isaac
9498135c38 Merge branch 'main' of github.com:oven-sh/bun into don/fix/http-outgoing-proto-2 2025-03-26 12:07:34 -07:00
Don Isaac
e170ac3206 minor cleanup 2025-03-25 17:00:42 -07:00
Don Isaac
e938bc59c9 get it working 2025-03-25 16:57:29 -07:00
Don Isaac
2cd9bb0c45 remove isStrict template 2025-03-25 14:56:54 -07:00
Don Isaac
d61cfee717 exception codes 2025-03-25 14:56:05 -07:00
13 changed files with 288 additions and 62 deletions

View File

@@ -113,7 +113,7 @@ cookies.set(cookie);
#### `delete(options: CookieStoreDeleteOptions): void`
Removes a cookie from the map. When applied to a Response, this adds a cookie with an empty string value and an expiry date in the past. A cookie will only delete succesfully on the browser if the domain and path is the same as it was when the cookie was created.
Removes a cookie from the map. When applied to a Response, this adds a cookie with an empty string value and an expiry date in the past. A cookie will only delete successfully on the browser if the domain and path is the same as it was when the cookie was created.
```ts
// Delete by name using default domain and path.

View File

@@ -1080,7 +1080,7 @@ function getRelevantTests(cwd) {
const filteredTests = [];
if (options["node-tests"]) {
tests = tests.filter(isNodeParallelTest);
tests = tests.filter(isNodeTest);
}
const isMatch = (testPath, filter) => {

View File

@@ -1231,6 +1231,30 @@ JSC::EncodedJSValue MISSING_PASSPHRASE(JSC::ThrowScope& throwScope, JSC::JSGloba
return {};
}
static JSC::JSObject* CREATE_HTTP_INVALID_HEADER_VALUE(JSC::JSGlobalObject* globalObject, const WTF::String& name, const WTF::StringView& value)
{
auto message = makeString("Invalid value \""_s, value, "\" for header \""_s, name, "\""_s);
return createError(globalObject, ErrorCode::ERR_HTTP_INVALID_HEADER_VALUE, message);
}
JSC::EncodedJSValue HTTP_INVALID_HEADER_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& name, const WTF::StringView& value)
{
throwScope.throwException(globalObject, JSValue(CREATE_HTTP_INVALID_HEADER_VALUE(globalObject, name, value)));
return {};
}
static JSC::JSObject* CREATE_HTTP_INVALID_TOKEN(JSC::JSGlobalObject* globalObject, const ASCIILiteral label, const WTF::StringView& token)
{
auto message = makeString(label, " must be a valid HTTP token [\""_s, token, "\"]"_s);
return createError(globalObject, ErrorCode::ERR_INVALID_HTTP_TOKEN, message);
}
JSC::EncodedJSValue HTTP_INVALID_TOKEN(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const ASCIILiteral label, const WTF::StringView& token)
{
throwScope.throwException(globalObject, JSValue(CREATE_HTTP_INVALID_TOKEN(globalObject, label, token)));
return {};
}
} // namespace ERR
static JSC::JSValue ERR_INVALID_ARG_TYPE(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, JSValue arg0, JSValue arg1, JSValue arg2)
@@ -1900,13 +1924,12 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject
case Bun::ErrorCode::ERR_HTTP_INVALID_HEADER_VALUE: {
auto arg0 = callFrame->argument(1);
auto str0 = arg0.toWTFString(globalObject);
auto value = arg0.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto arg1 = callFrame->argument(2);
auto str1 = arg1.toWTFString(globalObject);
auto headerName = arg1.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto message = makeString("Invalid value \""_s, str0, "\" for header \""_s, str1, "\""_s);
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP_INVALID_HEADER_VALUE, message));
return JSC::JSValue::encode(ERR::CREATE_HTTP_INVALID_HEADER_VALUE(globalObject, headerName, StringView(value)));
}
case Bun::ErrorCode::ERR_HTTP_HEADERS_SENT: {

View File

@@ -117,6 +117,11 @@ JSC::EncodedJSValue CRYPTO_INVALID_KEYLEN(JSC::ThrowScope&, JSC::JSGlobalObject*
JSC::EncodedJSValue CRYPTO_INVALID_STATE(JSC::ThrowScope&, JSC::JSGlobalObject*, WTF::ASCIILiteral message);
JSC::EncodedJSValue CRYPTO_INVALID_MESSAGELEN(JSC::ThrowScope&, JSC::JSGlobalObject*);
// HTTP
JSC::EncodedJSValue HTTP_INVALID_HEADER_VALUE(JSC::ThrowScope&, JSC::JSGlobalObject*, const WTF::String& name, const WTF::StringView& value);
// Code is `INVALID_HTTP_TOKEN`.
JSC::EncodedJSValue HTTP_INVALID_TOKEN(JSC::ThrowScope&, JSC::JSGlobalObject*, const ASCIILiteral label, const WTF::StringView& token);
// URL
/// `URL must be of scheme {expectedScheme}`

View File

@@ -74,6 +74,11 @@ enum ExceptionCode {
InvalidThisError,
InvalidURLError,
// http
InvalidHTTPTokenError,
InvalidHTTPHeaderValueError,
InvalidCharError,
};
} // namespace WebCore

View File

@@ -182,6 +182,15 @@ JSValue createDOMException(JSGlobalObject* lexicalGlobalObject, ExceptionCode ec
case ExceptionCode::InvalidURLError:
return Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_URL, message.isEmpty() ? "Invalid URL"_s : message);
case ExceptionCode::InvalidHTTPTokenError:
return Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_HTTP_TOKEN, message);
case ExceptionCode::InvalidHTTPHeaderValueError:
return Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_HTTP_INVALID_HEADER_VALUE, message);
case ExceptionCode::InvalidCharError:
return Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_CHAR, message);
default: {
// FIXME: All callers to createDOMException need to pass in the correct global object.
// For now, we're going to assume the lexicalGlobalObject. Which is wrong in cases like this:

View File

@@ -21,6 +21,7 @@
#include <JavaScriptCore/LazyPropertyInlines.h>
#include <JavaScriptCore/VMTrapsInlines.h>
#include "JSSocketAddressDTO.h"
#include "ErrorCode.h"
extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest, int* port, bool* is_ipv6);
extern "C" uint64_t uws_res_get_local_address_info(void* res, const char** dest, int* port, bool* is_ipv6);
@@ -1375,47 +1376,58 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetHeader, (JSGlobalObject * globalObject, CallFr
JSValue nameValue = callFrame->argument(1);
JSValue valueValue = callFrame->argument(2);
if (auto* headers = jsDynamicCast<WebCore::JSFetchHeaders*>(headersValue)) {
auto* headers = jsDynamicCast<WebCore::JSFetchHeaders*>(headersValue);
if (UNLIKELY(!headers)) {
return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "headers"_s, "FetchHeaders"_s, headersValue);
}
if (nameValue.isString()) {
String name = nameValue.toWTFString(globalObject);
FetchHeaders* impl = &headers->wrapped();
if (!nameValue.isString()) {
auto* nameString = nameValue.toString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
return Bun::ERR::HTTP_INVALID_TOKEN(scope, globalObject, "Header name"_s, nameString->view(globalObject));
}
if (valueValue.isUndefined())
// SAFETY: will not throw b/c we already checked it's a string, meaning it's just static cast
String name = nameValue.toWTFString(globalObject);
FetchHeaders* impl = &headers->wrapped();
// NOTE: null is valid
if (valueValue.isUndefined()) {
return Bun::ERR::HTTP_INVALID_HEADER_VALUE(scope, globalObject, name, StringView("undefined"_s));
}
if (isArray(globalObject, valueValue)) {
auto* array = jsCast<JSArray*>(valueValue);
unsigned length = array->length();
if (length > 0) {
JSValue item = array->getIndex(globalObject, 0);
if (UNLIKELY(scope.exception()))
return JSValue::encode(jsUndefined());
if (isArray(globalObject, valueValue)) {
auto* array = jsCast<JSArray*>(valueValue);
unsigned length = array->length();
if (length > 0) {
JSValue item = array->getIndex(globalObject, 0);
if (UNLIKELY(scope.exception()))
return JSValue::encode(jsUndefined());
auto value = item.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
impl->set(name, value);
RETURN_IF_EXCEPTION(scope, {});
}
for (unsigned i = 1; i < length; ++i) {
JSValue value = array->getIndex(globalObject, i);
if (UNLIKELY(scope.exception()))
return JSValue::encode(jsUndefined());
auto string = value.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
impl->append(name, string);
RETURN_IF_EXCEPTION(scope, {});
}
RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined()));
return JSValue::encode(jsUndefined());
}
auto value = valueValue.toWTFString(globalObject);
auto value = item.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
impl->set(name, value);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsUndefined());
}
for (unsigned i = 1; i < length; ++i) {
JSValue value = array->getIndex(globalObject, i);
if (UNLIKELY(scope.exception()))
return JSValue::encode(jsUndefined());
auto string = value.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
impl->append(name, string);
RETURN_IF_EXCEPTION(scope, {});
}
RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined()));
return JSValue::encode(jsUndefined());
}
auto value = valueValue.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto result = impl->set(name, value);
RETURN_IF_EXCEPTION(scope, {});
if (result.hasException()) {
scope.throwException(globalObject, WebCore::createDOMException(*globalObject, result.releaseException()));
}
return JSValue::encode(jsUndefined());

View File

@@ -43,11 +43,22 @@ static void removePrivilegedNoCORSRequestHeaders(HTTPHeaderMap& headers)
headers.remove(HTTPHeaderName::Range);
}
template<typename T>
static inline Exception invalidHTTPToken(const T& name)
{
return Exception { InvalidHTTPTokenError, makeString("Header name must be a valid HTTP token [\""_s, name, "\"]"_s) };
}
static inline Exception invalidHeaderValue(const StringView& name, const StringView& value)
{
return Exception { InvalidCharError, makeString("Invalid value '"_s, value, "' for header '"_s, name, "'"_s) };
}
static ExceptionOr<bool> canWriteHeader(const HTTPHeaderName name, const String& value, const String& combinedValue, FetchHeaders::Guard guard)
{
ASSERT(value.isEmpty() || (!isHTTPSpace(value[0]) && !isHTTPSpace(value[value.length() - 1])));
if (!isValidHTTPHeaderValue((value)))
return Exception { TypeError, makeString("Header '"_s, name, "' has invalid value: '"_s, value, "'"_s) };
if (!isValidHTTPHeaderValue(value))
return invalidHeaderValue(httpHeaderNameString(name), value);
if (guard == FetchHeaders::Guard::Immutable)
return Exception { TypeError, "Headers object's guard is 'immutable'"_s };
return true;
@@ -55,11 +66,13 @@ static ExceptionOr<bool> canWriteHeader(const HTTPHeaderName name, const String&
static ExceptionOr<bool> canWriteHeader(const String& name, const String& value, const String& combinedValue, FetchHeaders::Guard guard)
{
// TODO: consolidate error messages with ErrorCode.cpp
if (!isValidHTTPToken(name))
return Exception { TypeError, makeString("Invalid header name: '"_s, name, "'"_s) };
return invalidHTTPToken(name);
ASSERT(value.isEmpty() || (!isHTTPSpace(value[0]) && !isHTTPSpace(value[value.length() - 1])));
if (!isValidHTTPHeaderValue((value)))
return Exception { TypeError, makeString("Header '"_s, name, "' has invalid value: '"_s, value, "'"_s) };
if (!isValidHTTPHeaderValue(value))
return invalidHeaderValue(name, value);
if (guard == FetchHeaders::Guard::Immutable)
return Exception { TypeError, "Headers object's guard is 'immutable'"_s };
return true;
@@ -113,9 +126,6 @@ static ExceptionOr<void> appendToHeaderMap(const String& name, const String& val
if (!headers.setIndex(index, combinedValue))
headers.set(name, combinedValue);
// if (guard == FetchHeaders::Guard::RequestNoCors)
// removePrivilegedNoCORSRequestHeaders(headers);
return {};
}
@@ -208,7 +218,7 @@ ExceptionOr<void> FetchHeaders::append(const String& name, const String& value)
ExceptionOr<void> FetchHeaders::remove(const StringView name)
{
if (!isValidHTTPToken(name))
return Exception { TypeError, makeString("Invalid header name: '"_s, name, "'"_s) };
return invalidHTTPToken(name);
if (m_guard == FetchHeaders::Guard::Immutable)
return Exception { TypeError, "Headers object's guard is 'immutable'"_s };
if (m_guard == FetchHeaders::Guard::Request && isForbiddenHeaderName(name))
@@ -235,14 +245,14 @@ size_t FetchHeaders::memoryCost() const
ExceptionOr<String> FetchHeaders::get(const StringView name) const
{
if (!isValidHTTPToken(name))
return Exception { TypeError, makeString("Invalid header name: '"_s, name, "'"_s) };
return invalidHTTPToken(name);
return m_headers.get(name);
}
ExceptionOr<bool> FetchHeaders::has(const StringView name) const
{
if (!isValidHTTPToken(name))
return Exception { TypeError, makeString("Invalid header name: '"_s, name, '"') };
return invalidHTTPToken(name);
return m_headers.contains(name);
}

View File

@@ -139,7 +139,7 @@ bool isValidHTTPHeaderValue(const StringView& value)
}
} else {
for (unsigned i = 0; i < value.length(); ++i) {
c = value[i];
c = value.characterAt(i);
if (c == 0x00 || c == 0x0A || c == 0x0D || c > 0x7F)
return false;
}

View File

@@ -67,7 +67,7 @@ const kEmptyObject = Object.freeze(Object.create(null));
const { kDeprecatedReplySymbol } = require("internal/http");
const EventEmitter: typeof import("node:events").EventEmitter = require("node:events");
const { isTypedArray, isArrayBuffer } = require("node:util/types");
const { isTypedArray, isArrayBuffer, isUint8Array } = require("node:util/types");
const { Duplex, Readable, Stream } = require("node:stream");
const { isPrimary } = require("internal/cluster/isPrimary");
const { kAutoDestroyed } = require("internal/shared");
@@ -119,6 +119,7 @@ function isAbortError(err) {
}
const ObjectDefineProperty = Object.defineProperty;
const ObjectEntries = Object.entries;
const GlobalPromise = globalThis.Promise;
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
@@ -143,7 +144,7 @@ const validateHeaderValue = (name, value) => {
throw $ERR_HTTP_INVALID_HEADER_VALUE(value, name);
}
if (checkInvalidHeaderChar(value)) {
throw $ERR_INVALID_CHAR("header content", name);
throw $ERR_INVALID_CHAR("Header content", name);
}
};
@@ -1637,6 +1638,16 @@ const OutgoingMessagePrototype = {
encoding = undefined;
}
hasServerResponseFinished(this, chunk, callback);
if (chunk === null) {
throw $ERR_STREAM_NULL_VALUES();
} else if (typeof chunk !== "string" && !isUint8Array(chunk)) {
throw $ERR_INVALID_ARG_TYPE("chunk", ["string", "Buffer", "Uint8Array"], chunk);
}
if (!this._header) {
this._implicitHeader();
}
if (chunk) {
const len = Buffer.byteLength(chunk, encoding || (typeof chunk === "string" ? "utf8" : "buffer"));
if (len > 0) {
@@ -1644,6 +1655,7 @@ const OutgoingMessagePrototype = {
this.outputData.push(chunk);
}
}
return this.writableHighWaterMark >= this.outputSize;
},
@@ -1674,10 +1686,13 @@ const OutgoingMessagePrototype = {
headers.delete(name);
},
setHeader(name, value) {
validateHeaderName(name);
setHeader(name: string, value: unknown) {
if (this._header) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
const headers = (this[headersSymbol] ??= new Headers());
setHeader(headers, name, value);
setHeader(headers, name, value as any);
return this;
},
@@ -1697,6 +1712,17 @@ const OutgoingMessagePrototype = {
},
addTrailers(headers) {
if (!$isObject(headers)) {
throw $ERR_INVALID_ARG_TYPE("headers", "Object", headers);
}
for (const [key, value] of ObjectEntries(headers)) {
if (checkInvalidHeaderChar(key)) {
throw $ERR_INVALID_HTTP_TOKEN("Trailer name", key);
}
if (checkInvalidHeaderChar(value)) {
throw $ERR_INVALID_CHAR("trailer content", key);
}
}
throw new Error("not implemented");
},

View File

@@ -0,0 +1,135 @@
'use strict';
require('../common');
const assert = require('assert');
const http = require('http');
const OutgoingMessage = http.OutgoingMessage;
const ClientRequest = http.ClientRequest;
const ServerResponse = http.ServerResponse;
assert.strictEqual(
typeof ClientRequest.prototype._implicitHeader, 'function');
assert.strictEqual(
typeof ServerResponse.prototype._implicitHeader, 'function');
// validateHeader
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader();
}, {
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
message: 'Header name must be a valid HTTP token ["undefined"]'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader('test');
}, {
code: 'ERR_HTTP_INVALID_HEADER_VALUE',
name: 'TypeError',
message: 'Invalid value "undefined" for header "test"'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader(404);
}, {
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
message: 'Header name must be a valid HTTP token ["404"]'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader.call({ _header: 'test' }, 'test', 'value');
}, {
code: 'ERR_HTTP_HEADERS_SENT',
name: 'Error',
message: 'Cannot set headers after they are sent to the client'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader('200', 'あ');
}, {
code: 'ERR_INVALID_CHAR',
name: 'TypeError',
message: "Invalid value 'あ' for header '200'"
});
// write
{
const outgoingMessage = new OutgoingMessage();
assert.throws(
() => {
outgoingMessage.write('');
},
{
code: 'ERR_METHOD_NOT_IMPLEMENTED',
name: 'Error',
message: 'The _implicitHeader() method is not implemented'
}
);
}
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.write.call({ _header: 'test', _hasBody: 'test' });
}, {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "chunk" argument must be of type string, Buffer, or Uint8Array. Received undefined'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.write.call({ _header: 'test', _hasBody: 'test' }, 1);
}, {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "chunk" argument must be of type string, Buffer, or Uint8Array. Received type number (1)'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.write.call({ _header: 'test', _hasBody: 'test' }, null);
}, {
code: 'ERR_STREAM_NULL_VALUES',
name: 'TypeError'
});
// addTrailers()
// The `Error` comes from the JavaScript engine so confirm that it is a
// `TypeError` but do not check the message. It will be different in different
// JavaScript engines.
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.addTrailers();
}, TypeError);
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.addTrailers({ 'あ': 'value' });
}, {
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
message: 'Trailer name must be a valid HTTP token ["あ"]'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.addTrailers({ 404: 'あ' });
}, {
code: 'ERR_INVALID_CHAR',
name: 'TypeError',
message: 'Invalid character in trailer content ["404"]'
});
{
const outgoingMessage = new OutgoingMessage();
assert.strictEqual(outgoingMessage.destroyed, false);
outgoingMessage.destroy();
assert.strictEqual(outgoingMessage.destroyed, true);
}

View File

@@ -23,13 +23,14 @@ describe("Headers", async () => {
});
it("Header names must be valid", async () => {
expect(() => fetch(url, { headers: { "a\tb:c": "foo" } })).toThrow("Invalid header name: 'a\tb:c'");
expect(() => fetch(url, { headers: { "❤️": "foo" } })).toThrow("Invalid header name: '❤️'");
const prefix = "Header name must be a valid HTTP token";
expect(() => fetch(url, { headers: { "a\tb:c": "foo" } })).toThrow(prefix + ' ["a\tb:c"]');
expect(() => fetch(url, { headers: { "❤️": "foo" } })).toThrow(prefix + ' ["❤️"]');
});
it("Header values must be valid", async () => {
expect(() => fetch(url, { headers: { "x-test": "\0" } })).toThrow("Header 'x-test' has invalid value: '\0'");
expect(() => fetch(url, { headers: { "x-test": "❤️" } })).toThrow("Header 'x-test' has invalid value: '❤️'");
expect(() => fetch(url, { headers: { "x-test": "\0" } })).toThrow("Invalid value '\0' for header 'x-test'");
expect(() => fetch(url, { headers: { "x-test": "❤️" } })).toThrow("Invalid value '❤️' for header 'x-test'");
});
it("repro 1602", async () => {

View File

@@ -71,7 +71,7 @@ test("fetch() with subclass containing invalid HTTP headers throws without crash
const request = new MyRequest("https://example.com", {}, "https://example.com");
expect(request.method).toBe("POST");
expect(() => fetch(request)).toThrow("Invalid header name");
expect(() => fetch(request)).toThrow("Header name must be a valid HTTP token");
// quick gc test
for (let i = 0; i < 1e4; i++) {
@@ -80,5 +80,5 @@ test("fetch() with subclass containing invalid HTTP headers throws without crash
} catch (e) {}
}
expect(() => fetch(request)).toThrow("Invalid header name");
expect(() => fetch(request)).toThrow("Header name must be a valid HTTP token");
});