fix(BunRequest): make clone() return a BunRequest (#19813)

This commit is contained in:
Seth Flynn
2025-05-24 02:37:47 -04:00
committed by GitHub
parent 392212b090
commit c024e73e6a
7 changed files with 107 additions and 1 deletions

View File

@@ -3304,6 +3304,8 @@ declare module "bun" {
interface BunRequest<T extends string = string> extends Request {
params: RouterTypes.ExtractRouteParams<T>;
readonly cookies: CookieMap;
clone(): BunRequest<T>;
}
interface GenericServeOptions {

View File

@@ -214,7 +214,16 @@ ExceptionOr<void> CookieMap::remove(const CookieStoreDeleteOptions& options)
return {};
}
size_t CookieMap::size() const
Ref<CookieMap> CookieMap::clone()
{
auto clone = adoptRef(*new CookieMap());
clone->m_originalCookies = m_originalCookies;
clone->m_modifiedCookies = m_modifiedCookies;
return clone;
}
size_t
CookieMap::size() const
{
size_t size = 0;
for (const auto& cookie : m_modifiedCookies) {

View File

@@ -38,6 +38,8 @@ public:
void set(Ref<Cookie>);
Ref<CookieMap> clone();
ExceptionOr<void> remove(const CookieStoreDeleteOptions& options);
JSC::JSValue toJSON(JSC::JSGlobalObject*) const;

View File

@@ -11,6 +11,7 @@
#include "JSCookieMap.h"
#include "Cookie.h"
#include "CookieMap.h"
#include "ErrorCode.h"
#include "JSDOMExceptionHandling.h"
namespace Bun {
@@ -18,9 +19,12 @@ namespace Bun {
static JSC_DECLARE_CUSTOM_GETTER(jsJSBunRequestGetParams);
static JSC_DECLARE_CUSTOM_GETTER(jsJSBunRequestGetCookies);
static JSC_DECLARE_HOST_FUNCTION(jsJSBunRequestClone);
static const HashTableValue JSBunRequestPrototypeValues[] = {
{ "params"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsJSBunRequestGetParams, nullptr } },
{ "cookies"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsJSBunRequestGetCookies, nullptr } },
{ "clone"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsJSBunRequestClone, 1 } }
};
JSBunRequest* JSBunRequest::create(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr, JSObject* params)
@@ -66,6 +70,44 @@ JSObject* JSBunRequest::cookies() const
return nullptr;
}
extern "C" void* Request__clone(void* internalZigRequestPointer, JSGlobalObject* globalObject);
JSBunRequest* JSBunRequest::clone(JSC::VM& vm, JSGlobalObject* globalObject)
{
auto throwScope = DECLARE_THROW_SCOPE(globalObject->vm());
auto* structure = createJSBunRequestStructure(vm, defaultGlobalObject(globalObject));
auto* clone = this->create(vm, structure, Request__clone(this->wrapped(), globalObject), nullptr);
// Cookies and params are deep copied as they can be changed between the clone and original
if (auto* params = this->params()) {
// TODO: Use JSC's internal `cloneObject()` if/when it's exposed
// https://github.com/oven-sh/WebKit/blob/c5e9b9e327194f520af2c28679adb0ea1fa902ad/Source/JavaScriptCore/runtime/JSGlobalObjectFunctions.cpp#L1018-L1099
auto* prototype = defaultGlobalObject(globalObject)->m_JSBunRequestParamsPrototype.get(globalObject);
auto* paramsClone = JSC::constructEmptyObject(globalObject, prototype);
auto propertyNames = PropertyNameArray(vm, JSC::PropertyNameMode::Strings, JSC::PrivateSymbolMode::Exclude);
JSObject::getOwnPropertyNames(params, globalObject, propertyNames, JSC::DontEnumPropertiesMode::Exclude);
for (auto& property : propertyNames) {
auto value = params->get(globalObject, property);
RETURN_IF_EXCEPTION(throwScope, nullptr);
paramsClone->putDirect(vm, property, value);
}
clone->setParams(paramsClone);
}
if (auto* wrapper = jsDynamicCast<JSCookieMap*>(this->cookies())) {
auto cookieMap = wrapper->protectedWrapped();
auto cookieMapClone = cookieMap->clone();
auto cookies = WebCore::toJSNewlyCreated(globalObject, jsCast<JSDOMGlobalObject*>(globalObject), WTFMove(cookieMapClone));
clone->setCookies(cookies.getObject());
}
RELEASE_AND_RETURN(throwScope, clone);
}
extern "C" void Request__setCookiesOnRequestContext(void* internalZigRequestPointer, CookieMap* cookieMap);
void JSBunRequest::setCookies(JSObject* cookies)
@@ -203,6 +245,22 @@ JSC_DEFINE_CUSTOM_GETTER(jsJSBunRequestGetCookies, (JSC::JSGlobalObject * global
return JSValue::encode(cookies);
}
JSC_DEFINE_HOST_FUNCTION(jsJSBunRequestClone, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = globalObject->vm();
auto throwScope = DECLARE_THROW_SCOPE(vm);
auto* request = jsDynamicCast<JSBunRequest*>(callFrame->thisValue());
if (!request) {
throwScope.throwException(globalObject, Bun::createInvalidThisError(globalObject, request, "BunRequest"));
RETURN_IF_EXCEPTION(throwScope, {});
}
auto clone = request->clone(vm, globalObject);
RELEASE_AND_RETURN(throwScope, JSValue::encode(clone));
}
Structure* createJSBunRequestStructure(JSC::VM& vm, Zig::GlobalObject* globalObject)
{
auto prototypeStructure = JSBunRequestPrototype::createStructure(vm, globalObject, globalObject->JSRequestPrototype());

View File

@@ -36,6 +36,8 @@ public:
JSObject* cookies() const;
void setCookies(JSObject* cookies);
JSBunRequest* clone(JSC::VM& vm, JSGlobalObject* globalObject);
private:
JSBunRequest(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr);
void finishCreation(JSC::VM& vm, JSObject* params);

View File

@@ -66,7 +66,12 @@ pub export fn Request__setTimeout(this: *Request, seconds: JSC.JSValue, globalTh
this.setTimeout(seconds.to(c_uint));
}
pub export fn Request__clone(this: *Request, globalThis: *JSC.JSGlobalObject) *Request {
return this.clone(bun.default_allocator, globalThis);
}
comptime {
_ = Request__clone;
_ = Request__getUWSRequest;
_ = Request__setInternalEventCallback;
_ = Request__setTimeout;

View File

@@ -0,0 +1,28 @@
import { expect, test } from "bun:test";
test("18547", async () => {
using serve = Bun.serve({
routes: {
"/:foo": request => {
request.cookies.set("sessionToken", "123456");
// Ensure cloned requests have the same cookies and params of the original
const clone = request.clone();
expect(clone.cookies.get("sessionToken")).toEqual("123456");
expect(clone.params.foo).toEqual("foo");
// And that changes made to the clone don't affect the original
clone.cookies.set("sessionToken", "654321");
expect(request.cookies.get("sessionToken")).toEqual("123456");
expect(clone.cookies.get("sessionToken")).toEqual("654321");
return new Response("OK");
},
},
});
const response = await fetch(`${serve.url}/foo`);
// Or the context of the original request
expect(response.headers.get("set-cookie")).toEqual("sessionToken=123456; Path=/; SameSite=Lax");
});