mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 23:18:47 +00:00
Compare commits
2 Commits
claude/rea
...
jarred/req
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c9171e57c | ||
|
|
b433e9000f |
@@ -62,6 +62,7 @@ Routes in `Bun.serve()` receive a `BunRequest` (which extends [`Request`](https:
|
||||
interface BunRequest<T extends string> extends Request {
|
||||
params: Record<T, string>;
|
||||
readonly cookies: CookieMap;
|
||||
readonly searchParams: URLSearchParams;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -132,6 +133,47 @@ Bun.serve({
|
||||
|
||||
Percent-encoded route parameter values are automatically decoded. Unicode characters are supported. Invalid unicode is replaced with the unicode replacement character `&0xFFFD;`.
|
||||
|
||||
#### Query parameters
|
||||
|
||||
Request query parameters (the part after `?` in the URL) are accessible via `request.searchParams`, which returns a standard [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object.
|
||||
|
||||
```ts
|
||||
import { serve } from "bun";
|
||||
|
||||
serve({
|
||||
routes: {
|
||||
"/search": req => {
|
||||
// URL: /search?q=bun&limit=10
|
||||
|
||||
// Get a single value
|
||||
const query = req.searchParams.get("q"); // "bun"
|
||||
|
||||
// Get a value with fallback
|
||||
const limit = req.searchParams.get("limit") || "20"; // "10"
|
||||
|
||||
// Check if parameter exists
|
||||
const hasFilter = req.searchParams.has("filter"); // false
|
||||
|
||||
// Get all values for a parameter (useful for repeated params)
|
||||
const tags = req.searchParams.getAll("tag"); // [] (empty if none)
|
||||
|
||||
// Convert to plain object
|
||||
const allParams = Object.fromEntries(req.searchParams); // { q: "bun", limit: "10" }
|
||||
|
||||
return Response.json({ query, limit: Number(limit), hasFilter, tags });
|
||||
},
|
||||
|
||||
// Works with route parameters too
|
||||
"/users/:id/posts": req => {
|
||||
const userId = req.params.id;
|
||||
const page = req.searchParams.get("page") || "1";
|
||||
|
||||
return Response.json({ userId, page: Number(page) });
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Static responses
|
||||
|
||||
Routes can also be `Response` objects (without the handler function). Bun.serve() optimizes it for zero-allocation dispatch - perfect for health checks, redirects, and fixed content:
|
||||
|
||||
59
packages/bun-types/bun.d.ts
vendored
59
packages/bun-types/bun.d.ts
vendored
@@ -3302,7 +3302,66 @@ declare module "bun" {
|
||||
}
|
||||
|
||||
interface BunRequest<T extends string = string> extends Request {
|
||||
/**
|
||||
* The `:param` part of the URL as an object. If the URL is `/users/:id`,
|
||||
* then `req.params` is `{ id: "123" }`. If the URL has no parameters, its
|
||||
* an empty object.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const server = Bun.serve({
|
||||
* routes: {
|
||||
* "/:foo/:bar": (req, server) => {
|
||||
* console.log(req.params.foo); // "bar"
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
* await fetch(`${server.url}/foo/bar`)
|
||||
* ```
|
||||
*/
|
||||
params: RouterTypes.ExtractRouteParams<T>;
|
||||
|
||||
/**
|
||||
* The `?query` part of the URL as a {@link URLSearchParams} object.
|
||||
*
|
||||
* If no query is provided, it returns an empty URLSearchParams object.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* const server = Bun.serve({
|
||||
* routes: {
|
||||
* "/": (req, server) => {
|
||||
* console.log(req.query.get("foo")); // "bar"
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* await fetch(`${server.url}/?foo=bar`)
|
||||
* ```
|
||||
*/
|
||||
readonly searchParams: URLSearchParams;
|
||||
|
||||
/**
|
||||
* The `Cookie` header as a {@link CookieMap} object.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const server = Bun.serve({
|
||||
* routes: {
|
||||
* "/": (req, server) => {
|
||||
* console.log(req.cookies.get("foo")); // "bar"
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* await fetch(`${server.url}`, {
|
||||
* headers: {
|
||||
* Cookie: "foo=bar"
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
readonly cookies: CookieMap;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,20 @@
|
||||
#include "Cookie.h"
|
||||
#include "CookieMap.h"
|
||||
#include "JSDOMExceptionHandling.h"
|
||||
|
||||
#include <bun-uws/src/App.h>
|
||||
#include "JSURLSearchParams.h"
|
||||
#include "URLSearchParams.h"
|
||||
#include <wtf/URLParser.h>
|
||||
namespace Bun {
|
||||
|
||||
extern "C" uWS::HttpRequest* Request__getUWSRequest(JSBunRequest*);
|
||||
|
||||
static JSC_DECLARE_CUSTOM_GETTER(jsJSBunRequestGetParams);
|
||||
static JSC_DECLARE_CUSTOM_GETTER(jsJSBunRequestGetCookies);
|
||||
static JSC_DECLARE_CUSTOM_GETTER(jsJSBunRequestGetQuery);
|
||||
|
||||
static const HashTableValue JSBunRequestPrototypeValues[] = {
|
||||
{ "searchParams"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsJSBunRequestGetQuery, nullptr } },
|
||||
{ "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 } },
|
||||
};
|
||||
@@ -66,6 +73,19 @@ JSObject* JSBunRequest::cookies() const
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
JSObject* JSBunRequest::query() const
|
||||
{
|
||||
if (m_query) {
|
||||
return m_query.get();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void JSBunRequest::setQuery(JSObject* query)
|
||||
{
|
||||
m_query.set(Base::vm(), this, query);
|
||||
}
|
||||
|
||||
extern "C" void Request__setCookiesOnRequestContext(void* internalZigRequestPointer, CookieMap* cookieMap);
|
||||
|
||||
void JSBunRequest::setCookies(JSObject* cookies)
|
||||
@@ -85,6 +105,7 @@ void JSBunRequest::finishCreation(JSC::VM& vm, JSObject* params)
|
||||
Base::finishCreation(vm);
|
||||
m_params.setMayBeNull(vm, this, params);
|
||||
m_cookies.clear();
|
||||
m_query.clear();
|
||||
Bun__JSRequest__calculateEstimatedByteSize(this->wrapped());
|
||||
|
||||
auto size = Request__estimatedSize(this->wrapped());
|
||||
@@ -98,6 +119,7 @@ void JSBunRequest::visitChildrenImpl(JSCell* cell, Visitor& visitor)
|
||||
Base::visitChildren(thisCallSite, visitor);
|
||||
visitor.append(thisCallSite->m_params);
|
||||
visitor.append(thisCallSite->m_cookies);
|
||||
visitor.append(thisCallSite->m_query);
|
||||
}
|
||||
|
||||
DEFINE_VISIT_CHILDREN(JSBunRequest);
|
||||
@@ -162,6 +184,73 @@ JSC_DEFINE_CUSTOM_GETTER(jsJSBunRequestGetParams, (JSC::JSGlobalObject * globalO
|
||||
return JSValue::encode(params);
|
||||
}
|
||||
|
||||
static JSValue createQueryObject(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSBunRequest* request)
|
||||
{
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
auto* uws = Request__getUWSRequest(request);
|
||||
auto* global = defaultGlobalObject(globalObject);
|
||||
|
||||
// First, try to get it from uWS::HttpRequest
|
||||
if (uws) {
|
||||
auto query = uws->getQuery();
|
||||
auto span = std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(query.data()), query.size());
|
||||
// This should always be URL-encoded
|
||||
WTF::String queryString = WTF::String::fromUTF8ReplacingInvalidSequences(span);
|
||||
auto searchParams = WebCore::URLSearchParams::create(queryString, nullptr);
|
||||
return WebCore::toJSNewlyCreated(global, global, WTFMove(searchParams));
|
||||
}
|
||||
|
||||
// Otherwise, get it by reading the url property.
|
||||
auto& names = builtinNames(vm);
|
||||
auto url = request->get(globalObject, names.urlPublicName());
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
auto* urlString = url.toString(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
auto view = urlString->view(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
// Figure out where the query string is
|
||||
const auto findQustionMark = view->find('?');
|
||||
WTF::StringView queryView;
|
||||
|
||||
if (findQustionMark != WTF::notFound) {
|
||||
queryView = view->substring(findQustionMark + 1, view->length() - findQustionMark - 1);
|
||||
}
|
||||
|
||||
// Parse the query string
|
||||
auto searchParams = queryView.length() > 0 ? WebCore::URLSearchParams::create(WTF::URLParser::parseURLEncodedForm(queryView)) : WebCore::URLSearchParams::create({});
|
||||
|
||||
// If for any reason that failed, throw an error
|
||||
if (searchParams.hasException()) [[unlikely]] {
|
||||
WebCore::propagateException(*globalObject, scope, searchParams.releaseException());
|
||||
return {};
|
||||
}
|
||||
|
||||
return WebCore::toJSNewlyCreated(global, global, searchParams.releaseReturnValue());
|
||||
}
|
||||
|
||||
JSC_DEFINE_CUSTOM_GETTER(jsJSBunRequestGetQuery, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
|
||||
{
|
||||
|
||||
JSBunRequest* request = jsDynamicCast<JSBunRequest*>(JSValue::decode(thisValue));
|
||||
if (!request)
|
||||
return JSValue::encode(jsUndefined());
|
||||
|
||||
if (auto* query = request->query()) {
|
||||
return JSValue::encode(query);
|
||||
}
|
||||
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
auto result = createQueryObject(vm, globalObject, request);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
request->setQuery(result.getObject());
|
||||
return JSValue::encode(result);
|
||||
}
|
||||
|
||||
JSC_DEFINE_CUSTOM_GETTER(jsJSBunRequestGetCookies, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
|
||||
{
|
||||
JSBunRequest* request = jsDynamicCast<JSBunRequest*>(JSValue::decode(thisValue));
|
||||
@@ -224,4 +313,23 @@ extern "C" EncodedJSValue Bun__getParamsIfBunRequest(JSC::EncodedJSValue thisVal
|
||||
return JSValue::encode({});
|
||||
}
|
||||
|
||||
extern "C" EncodedJSValue Bun__getQueryIfBunRequest(JSC::EncodedJSValue thisValue)
|
||||
{
|
||||
if (auto* request = jsDynamicCast<JSBunRequest*>(JSValue::decode(thisValue))) {
|
||||
if (auto* query = request->query()) {
|
||||
return JSValue::encode(query);
|
||||
}
|
||||
|
||||
auto* globalObject = request->globalObject();
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
auto result = createQueryObject(vm, globalObject, request);
|
||||
RETURN_IF_EXCEPTION(scope, encodedJSValue());
|
||||
request->setQuery(result.getObject());
|
||||
return JSValue::encode(result);
|
||||
}
|
||||
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
} // namespace Bun
|
||||
|
||||
@@ -36,12 +36,16 @@ public:
|
||||
JSObject* cookies() const;
|
||||
void setCookies(JSObject* cookies);
|
||||
|
||||
JSObject* query() const;
|
||||
void setQuery(JSObject* query);
|
||||
|
||||
private:
|
||||
JSBunRequest(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr);
|
||||
void finishCreation(JSC::VM& vm, JSObject* params);
|
||||
|
||||
mutable JSC::WriteBarrier<JSC::JSObject> m_params;
|
||||
mutable JSC::WriteBarrier<JSC::JSObject> m_cookies;
|
||||
mutable JSC::WriteBarrier<JSC::JSObject> m_query;
|
||||
};
|
||||
|
||||
JSC::Structure* createJSBunRequestStructure(JSC::VM&, Zig::GlobalObject*);
|
||||
|
||||
@@ -259,7 +259,7 @@ JSValue ServerRouteList::callRoute(Zig::GlobalObject* globalObject, uint32_t ind
|
||||
args.append(request);
|
||||
args.append(serverValue);
|
||||
|
||||
return AsyncContextFrame::call(globalObject, callback, serverValue, args);
|
||||
return AsyncContextFrame::profiledCall(globalObject, callback, serverValue, args);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue Bun__ServerRouteList__callRoute(
|
||||
|
||||
@@ -171,12 +171,14 @@ pub fn toJS(this: *Request, globalObject: *JSGlobalObject) JSValue {
|
||||
return js.toJSUnchecked(globalObject, this);
|
||||
}
|
||||
|
||||
extern "JS" fn Bun__getParamsIfBunRequest(this_value: JSValue) JSValue;
|
||||
extern "c" fn Bun__getParamsIfBunRequest(this_value: JSValue) JSValue;
|
||||
extern "c" fn Bun__getQueryIfBunRequest(this_value: JSValue) JSValue;
|
||||
|
||||
pub fn writeFormat(this: *Request, this_value: JSValue, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void {
|
||||
const Writer = @TypeOf(writer);
|
||||
|
||||
const params_object = Bun__getParamsIfBunRequest(this_value);
|
||||
const query_object = Bun__getQueryIfBunRequest(this_value);
|
||||
|
||||
const class_label = switch (params_object) {
|
||||
.zero => "Request",
|
||||
@@ -205,7 +207,16 @@ pub fn writeFormat(this: *Request, this_value: JSValue, comptime Formatter: type
|
||||
if (params_object.isCell()) {
|
||||
try formatter.writeIndent(Writer, writer);
|
||||
try writer.writeAll(comptime Output.prettyFmt("<r>params<d>:<r> ", enable_ansi_colors));
|
||||
try formatter.printAs(.Private, Writer, writer, params_object, .Object, enable_ansi_colors);
|
||||
try formatter.printAs(.Object, Writer, writer, params_object, .Object, enable_ansi_colors);
|
||||
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
if (query_object.isCell()) {
|
||||
try formatter.writeIndent(Writer, writer);
|
||||
try writer.writeAll(comptime Output.prettyFmt("<r>searchParams<d>:<r> ", enable_ansi_colors));
|
||||
const tag = if (Formatter == JSC.ConsoleObject.Formatter) .toJSON else .Object;
|
||||
try formatter.printAs(tag, Writer, writer, query_object, query_object.jsType(), enable_ansi_colors);
|
||||
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ const words: Record<string, { reason: string; limit?: number; regex?: boolean }>
|
||||
|
||||
[String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 240, regex: true },
|
||||
"usingnamespace": { reason: "Zig 0.15 will remove `usingnamespace`" },
|
||||
"catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1850 },
|
||||
"catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1851 },
|
||||
|
||||
"std.fs.Dir": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 180 },
|
||||
"std.fs.cwd": { reason: "Prefer bun.FD.cwd()", limit: 103 },
|
||||
|
||||
339
test/js/bun/http/bun-serve-query.test.ts
Normal file
339
test/js/bun/http/bun-serve-query.test.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import type { BunRequest, Server } from "bun";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "bun:test";
|
||||
|
||||
let server: Server;
|
||||
|
||||
afterAll(() => {
|
||||
server.stop(true);
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/echo-query": req => {
|
||||
return new Response("hello");
|
||||
},
|
||||
},
|
||||
});
|
||||
server.unref();
|
||||
});
|
||||
|
||||
describe("request.searchParams basic functionality", () => {
|
||||
beforeEach(() => {
|
||||
server.reload({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/echo-query": req => {
|
||||
if (!req.searchParams) {
|
||||
throw new Error("query is undefined");
|
||||
}
|
||||
if (!(req.searchParams instanceof URLSearchParams)) {
|
||||
throw new Error("query is not a URLSearchParams");
|
||||
}
|
||||
|
||||
console.log(req);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
// Convert URLSearchParams to plain object for easier assertions
|
||||
asObject: Object.fromEntries(req.searchParams),
|
||||
// Also test the raw URLSearchParams toString() result
|
||||
asString: req.searchParams.toString(),
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("handles simple query parameters", async () => {
|
||||
const res = await fetch(`${server.url}echo-query?foo=bar`);
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.asObject).toEqual({
|
||||
foo: "bar",
|
||||
});
|
||||
expect(data.asString).toBe("foo=bar");
|
||||
});
|
||||
|
||||
it("handles multiple query parameters", async () => {
|
||||
const res = await fetch(`${server.url}echo-query?foo=bar&baz=qux`);
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.asObject).toEqual({
|
||||
foo: "bar",
|
||||
baz: "qux",
|
||||
});
|
||||
expect(data.asString).toBe("foo=bar&baz=qux");
|
||||
});
|
||||
|
||||
it("handles empty query parameters", async () => {
|
||||
const res = await fetch(`${server.url}echo-query?empty=&foo=bar`);
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.asObject).toEqual({
|
||||
empty: "",
|
||||
foo: "bar",
|
||||
});
|
||||
expect(data.asString).toBe("empty=&foo=bar");
|
||||
});
|
||||
|
||||
it("handles key-only query parameters", async () => {
|
||||
const res = await fetch(`${server.url}echo-query?flag&foo=bar`);
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.asObject).toEqual({
|
||||
flag: "",
|
||||
foo: "bar",
|
||||
});
|
||||
expect(data.asString).toBe("flag=&foo=bar");
|
||||
});
|
||||
|
||||
it("handles no query parameters", async () => {
|
||||
const res = await fetch(`${server.url}echo-query`);
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.asObject).toEqual({});
|
||||
expect(data.asString).toBe("");
|
||||
});
|
||||
|
||||
it("handles encoded query parameters", async () => {
|
||||
const res = await fetch(`${server.url}echo-query?email=user%40example.com&message=hello%20world`);
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.asObject).toEqual({
|
||||
email: "user@example.com",
|
||||
message: "hello world",
|
||||
});
|
||||
});
|
||||
|
||||
it("handles unicode query parameters", async () => {
|
||||
const res = await fetch(`${server.url}echo-query?emoji=🦊&text=こんにちは`);
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.asObject).toEqual({
|
||||
emoji: "🦊",
|
||||
text: "こんにちは",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("request.searchParams URLSearchParams methods", () => {
|
||||
beforeEach(() => {
|
||||
server.reload({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/has": req => new Response(req.searchParams.has("key").toString()),
|
||||
"/get": req => new Response(req.searchParams.get("key") || "null"),
|
||||
"/getAll": req => new Response(JSON.stringify(req.searchParams.getAll("key"))),
|
||||
"/entries": req => {
|
||||
const entries = Array.from(req.searchParams.entries());
|
||||
return new Response(JSON.stringify(entries));
|
||||
},
|
||||
"/keys": req => {
|
||||
const keys = Array.from(req.searchParams.keys());
|
||||
return new Response(JSON.stringify(keys));
|
||||
},
|
||||
"/values": req => {
|
||||
const values = Array.from(req.searchParams.values());
|
||||
return new Response(JSON.stringify(values));
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("implements has() method", async () => {
|
||||
let res = await fetch(`${server.url}has?key=value`);
|
||||
expect(await res.text()).toBe("true");
|
||||
|
||||
res = await fetch(`${server.url}has?otherkey=value`);
|
||||
expect(await res.text()).toBe("false");
|
||||
});
|
||||
|
||||
it("implements get() method", async () => {
|
||||
let res = await fetch(`${server.url}get?key=value`);
|
||||
expect(await res.text()).toBe("value");
|
||||
|
||||
res = await fetch(`${server.url}get?otherkey=value`);
|
||||
expect(await res.text()).toBe("null");
|
||||
});
|
||||
|
||||
it("implements getAll() method for repeated parameters", async () => {
|
||||
const res = await fetch(`${server.url}getAll?key=value1&key=value2&key=value3`);
|
||||
const values = await res.json();
|
||||
expect(values).toEqual(["value1", "value2", "value3"]);
|
||||
});
|
||||
|
||||
it("implements entries() method", async () => {
|
||||
const res = await fetch(`${server.url}entries?a=1&b=2&c=3`);
|
||||
const entries = await res.json();
|
||||
expect(entries).toEqual([
|
||||
["a", "1"],
|
||||
["b", "2"],
|
||||
["c", "3"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("implements keys() method", async () => {
|
||||
const res = await fetch(`${server.url}keys?a=1&b=2&c=3`);
|
||||
const keys = await res.json();
|
||||
expect(keys).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
it("implements values() method", async () => {
|
||||
const res = await fetch(`${server.url}values?a=1&b=2&c=3`);
|
||||
const values = await res.json();
|
||||
expect(values).toEqual(["1", "2", "3"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("request.searchParams with route parameters", () => {
|
||||
beforeEach(() => {
|
||||
server.reload({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/users/:id": (req: BunRequest<"/users/:id">) => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
params: req.params,
|
||||
query: Object.fromEntries(req.searchParams),
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("combines route parameters with query parameters", async () => {
|
||||
const res = await fetch(`${server.url}users/123?sort=name&filter=active`);
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data).toEqual({
|
||||
params: { id: "123" },
|
||||
query: {
|
||||
sort: "name",
|
||||
filter: "active",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("request.searchParams manipulation", () => {
|
||||
beforeEach(() => {
|
||||
server.reload({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/append": req => {
|
||||
const query = req.searchParams;
|
||||
query.append("added", "value");
|
||||
return new Response(query.toString());
|
||||
},
|
||||
"/set": req => {
|
||||
const query = req.searchParams;
|
||||
query.set("key", "newvalue");
|
||||
return new Response(query.toString());
|
||||
},
|
||||
"/delete": req => {
|
||||
const query = req.searchParams;
|
||||
query.delete("key");
|
||||
return new Response(query.toString());
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("allows appending new parameters", async () => {
|
||||
const res = await fetch(`${server.url}append?existing=value`);
|
||||
expect(await res.text()).toBe("existing=value&added=value");
|
||||
});
|
||||
|
||||
it("allows setting parameter values", async () => {
|
||||
const res = await fetch(`${server.url}set?key=oldvalue&other=value`);
|
||||
expect(await res.text()).toBe("key=newvalue&other=value");
|
||||
});
|
||||
|
||||
it("allows deleting parameters", async () => {
|
||||
const res = await fetch(`${server.url}delete?key=value&other=value`);
|
||||
expect(await res.text()).toBe("other=value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("request.searchParams in async handlers", () => {
|
||||
beforeEach(() => {
|
||||
server.reload({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/async-echo-query": async req => {
|
||||
await Bun.sleep(1);
|
||||
if (!req.searchParams) {
|
||||
throw new Error("query is undefined in async handler");
|
||||
}
|
||||
if (!(req.searchParams instanceof URLSearchParams)) {
|
||||
throw new Error("query is not a URLSearchParams in async handler");
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
asObject: Object.fromEntries(req.searchParams),
|
||||
asString: req.searchParams.toString(),
|
||||
}),
|
||||
);
|
||||
},
|
||||
"/async-methods": async req => {
|
||||
await Bun.sleep(1);
|
||||
const result = {
|
||||
has: req.searchParams.has("key"),
|
||||
get: req.searchParams.get("key"),
|
||||
getAll: req.searchParams.getAll("key"),
|
||||
entries: Array.from(req.searchParams.entries()),
|
||||
keys: Array.from(req.searchParams.keys()),
|
||||
values: Array.from(req.searchParams.values()),
|
||||
};
|
||||
return new Response(JSON.stringify(result));
|
||||
},
|
||||
"/async-manipulation": async req => {
|
||||
await Bun.sleep(1);
|
||||
const query = req.searchParams;
|
||||
query.append("added", "value");
|
||||
query.set("existing", "updated");
|
||||
if (query.has("delete")) {
|
||||
query.delete("delete");
|
||||
}
|
||||
return new Response(query.toString());
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("handles query parameters in async handlers", async () => {
|
||||
const res = await fetch(`${server.url}async-echo-query?foo=bar&baz=qux`);
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.asObject).toEqual({
|
||||
foo: "bar",
|
||||
baz: "qux",
|
||||
});
|
||||
expect(data.asString).toBe("foo=bar&baz=qux");
|
||||
});
|
||||
|
||||
it("supports URLSearchParams methods in async handlers", async () => {
|
||||
const res = await fetch(`${server.url}async-methods?key=value&other=test`);
|
||||
expect(res.status).toBe(200);
|
||||
const data = await res.json();
|
||||
expect(data.has).toBe(true);
|
||||
expect(data.get).toBe("value");
|
||||
expect(data.getAll).toEqual(["value"]);
|
||||
expect(data.entries).toEqual([
|
||||
["key", "value"],
|
||||
["other", "test"],
|
||||
]);
|
||||
expect(data.keys).toEqual(["key", "other"]);
|
||||
expect(data.values).toEqual(["value", "test"]);
|
||||
});
|
||||
|
||||
it("allows manipulation in async handlers", async () => {
|
||||
const res = await fetch(`${server.url}async-manipulation?existing=original&delete=remove`);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toBe("existing=updated&added=value");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user