mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Compare commits
6 Commits
bun-v1.3.4
...
riskymh/bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e1c787546 | ||
|
|
73415e1154 | ||
|
|
7281dfd0ed | ||
|
|
9ce27b92ec | ||
|
|
229facad76 | ||
|
|
0587c598b7 |
@@ -297,6 +297,9 @@ pub const Stdio = union(enum) {
|
||||
},
|
||||
|
||||
.Blob, .WTFStringImpl, .InternalBlob => unreachable, // handled above.
|
||||
.HTMLBundle => {
|
||||
return globalThis.throwInvalidArguments("HTMLBundle cannot be used as stdin", .{});
|
||||
},
|
||||
.Locked => {
|
||||
if (is_sync) {
|
||||
return globalThis.throwInvalidArguments("ReadableStream cannot be used in sync mode", .{});
|
||||
|
||||
@@ -198,6 +198,37 @@ pub const AnyRoute = union(enum) {
|
||||
}
|
||||
}
|
||||
|
||||
if (argument.as(jsc.WebCore.Response)) |response| {
|
||||
if (response.body.value == .HTMLBundle) {
|
||||
const html_bundle = response.body.value.HTMLBundle;
|
||||
const needs_custom = response.init.headers != null or response.statusCode() != 200;
|
||||
|
||||
if (needs_custom) {
|
||||
var route = HTMLBundle.Route.init(html_bundle);
|
||||
|
||||
if (response.init.headers) |headers| {
|
||||
route.data.custom_headers = bun.http.Headers.from(headers, bun.default_allocator, .{}) catch bun.outOfMemory();
|
||||
}
|
||||
|
||||
const status = response.statusCode();
|
||||
if (status != 200) {
|
||||
route.data.custom_status = status;
|
||||
}
|
||||
|
||||
return .{ .html = route };
|
||||
} else {
|
||||
const entry = init_ctx.dedupe_html_bundle_map.getOrPut(html_bundle) catch bun.outOfMemory();
|
||||
|
||||
if (!entry.found_existing) {
|
||||
entry.value_ptr.* = HTMLBundle.Route.init(html_bundle);
|
||||
return .{ .html = entry.value_ptr.* };
|
||||
} else {
|
||||
return .{ .html = entry.value_ptr.dupeRef() };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (try bundledHTMLManifestFromJS(argument, init_ctx)) |html_route| {
|
||||
return html_route;
|
||||
}
|
||||
|
||||
@@ -69,11 +69,18 @@ pub const Route = struct {
|
||||
method: bun.http.Method.Set,
|
||||
} = .any,
|
||||
|
||||
/// Custom headers & status code to apply to the HTML response
|
||||
custom_headers: ?bun.http.Headers = null,
|
||||
custom_status: ?u16 = null,
|
||||
|
||||
pub fn memoryCost(this: *const Route) usize {
|
||||
var cost: usize = 0;
|
||||
cost += @sizeOf(Route);
|
||||
cost += this.pending_responses.items.len * @sizeOf(PendingResponse);
|
||||
cost += this.state.memoryCost();
|
||||
if (this.custom_headers) |headers| {
|
||||
cost += headers.memoryCost();
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
@@ -92,6 +99,9 @@ pub const Route = struct {
|
||||
this.pending_responses.deinit(bun.default_allocator);
|
||||
this.bundle.deref();
|
||||
this.state.deinit();
|
||||
if (this.custom_headers) |*headers| {
|
||||
headers.deinit();
|
||||
}
|
||||
bun.destroy(this);
|
||||
}
|
||||
|
||||
@@ -415,6 +425,19 @@ pub const Route = struct {
|
||||
|
||||
const html_route: *StaticRoute = this_html_route orelse @panic("Internal assertion failure: HTML entry point not found in HTMLBundle.");
|
||||
const html_route_clone = html_route.clone(globalThis) catch bun.outOfMemory();
|
||||
|
||||
if (this.custom_headers) |*custom_headers| {
|
||||
const entries = custom_headers.entries.slice();
|
||||
const names = entries.items(.name);
|
||||
const values = entries.items(.value);
|
||||
for (names, values) |name_ptr, value_ptr| {
|
||||
html_route_clone.headers.append(custom_headers.asStr(name_ptr), custom_headers.asStr(value_ptr)) catch bun.outOfMemory();
|
||||
}
|
||||
}
|
||||
if (this.custom_status) |status| {
|
||||
html_route_clone.status_code = status;
|
||||
}
|
||||
|
||||
this.state = .{ .html = html_route_clone };
|
||||
|
||||
if (!(server.reloadStaticRoutes() catch bun.outOfMemory())) {
|
||||
|
||||
@@ -1439,6 +1439,12 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
resp.writeHeaderInt("content-length", 0);
|
||||
this.endWithoutBody(this.shouldCloseConnection());
|
||||
},
|
||||
.HTMLBundle => {
|
||||
// HTMLBundle cannot be used in HEAD response
|
||||
this.renderMetadata();
|
||||
resp.writeHeaderInt("content-length", 0);
|
||||
this.endWithoutBody(this.shouldCloseConnection());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1325,6 +1325,10 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
|
||||
=> {
|
||||
break :brk response.body.use();
|
||||
},
|
||||
.HTMLBundle => {
|
||||
destination_blob.detach();
|
||||
return globalThis.throwInvalidArguments("HTMLBundle cannot be written to a file", .{});
|
||||
},
|
||||
.Error => |*err_ref| {
|
||||
destination_blob.detach();
|
||||
_ = response.body.value.use();
|
||||
@@ -1386,6 +1390,10 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
|
||||
=> {
|
||||
break :brk request.body.value.use();
|
||||
},
|
||||
.HTMLBundle => {
|
||||
destination_blob.detach();
|
||||
return globalThis.throwInvalidArguments("HTMLBundle cannot be written to a file", .{});
|
||||
},
|
||||
.Error => |*err_ref| {
|
||||
destination_blob.detach();
|
||||
_ = request.body.value.use();
|
||||
|
||||
@@ -268,6 +268,7 @@ pub const Value = union(Tag) {
|
||||
Empty,
|
||||
Error: ValueError,
|
||||
Null,
|
||||
HTMLBundle: *bun.jsc.API.HTMLBundle,
|
||||
|
||||
// We may not have all the data yet
|
||||
// So we can't know for sure if it's empty or not
|
||||
@@ -280,6 +281,7 @@ pub const Value = union(Tag) {
|
||||
.Blob => this.Blob.size == 0,
|
||||
.WTFStringImpl => this.WTFStringImpl.length() == 0,
|
||||
.Error, .Locked => false,
|
||||
.HTMLBundle => false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -440,6 +442,7 @@ pub const Value = union(Tag) {
|
||||
Empty,
|
||||
Error,
|
||||
Null,
|
||||
HTMLBundle,
|
||||
};
|
||||
|
||||
// pub const empty = Value{ .Empty = {} };
|
||||
@@ -523,6 +526,9 @@ pub const Value = union(Tag) {
|
||||
// TODO: handle error properly
|
||||
return jsc.WebCore.ReadableStream.empty(globalThis);
|
||||
},
|
||||
.HTMLBundle => {
|
||||
return globalThis.throwInvalidArguments("HTMLBundle cannot be used as a Response body", .{});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,6 +604,13 @@ pub const Value = union(Tag) {
|
||||
}
|
||||
}
|
||||
|
||||
if (value.as(bun.jsc.API.HTMLBundle)) |html_bundle| {
|
||||
html_bundle.ref();
|
||||
return Body.Value{
|
||||
.HTMLBundle = html_bundle,
|
||||
};
|
||||
}
|
||||
|
||||
value.ensureStillAlive();
|
||||
|
||||
if (try jsc.WebCore.ReadableStream.fromJS(value, globalThis)) |readable| {
|
||||
@@ -858,6 +871,7 @@ pub const Value = union(Tag) {
|
||||
},
|
||||
// .InlineBlob => .{ .InlineBlob = this.InlineBlob },
|
||||
.Locked => this.Locked.toAnyBlobAllowPromise() orelse AnyBlob{ .Blob = .{} },
|
||||
.HTMLBundle => .{ .Blob = Blob.initEmpty(undefined) },
|
||||
else => .{ .Blob = Blob.initEmpty(undefined) },
|
||||
};
|
||||
|
||||
@@ -875,6 +889,7 @@ pub const Value = union(Tag) {
|
||||
.WTFStringImpl => .{ .WTFStringImpl = this.WTFStringImpl },
|
||||
// .InlineBlob => .{ .InlineBlob = this.InlineBlob },
|
||||
.Locked => this.Locked.toAnyBlobAllowPromise() orelse AnyBlob{ .Blob = .{} },
|
||||
.HTMLBundle => .{ .Blob = Blob.initEmpty(undefined) },
|
||||
else => .{ .Blob = Blob.initEmpty(undefined) },
|
||||
};
|
||||
|
||||
@@ -963,6 +978,11 @@ pub const Value = union(Tag) {
|
||||
if (tag == .Error) {
|
||||
this.Error.deinit();
|
||||
}
|
||||
|
||||
if (tag == .HTMLBundle) {
|
||||
this.HTMLBundle.deref();
|
||||
this.* = Value{ .Null = {} };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tee(this: *Value, globalThis: *jsc.JSGlobalObject) bun.JSError!Value {
|
||||
@@ -1093,6 +1113,10 @@ pub fn Mixin(comptime Type: type) type {
|
||||
return handleBodyAlreadyUsed(globalObject);
|
||||
}
|
||||
|
||||
if (value.* == .HTMLBundle) {
|
||||
return globalObject.throwInvalidArguments("HTMLBundle cannot be used as a Response body", .{});
|
||||
}
|
||||
|
||||
if (value.* == .Locked) {
|
||||
if (value.Locked.action != .none or value.Locked.isDisturbed(Type, globalObject, callframe.this())) {
|
||||
return handleBodyAlreadyUsed(globalObject);
|
||||
@@ -1149,6 +1173,10 @@ pub fn Mixin(comptime Type: type) type {
|
||||
return handleBodyAlreadyUsed(globalObject);
|
||||
}
|
||||
|
||||
if (value.* == .HTMLBundle) {
|
||||
return globalObject.throwInvalidArguments("HTMLBundle cannot be used as a Response body", .{});
|
||||
}
|
||||
|
||||
if (value.* == .Locked) {
|
||||
if (value.Locked.action != .none or value.Locked.isDisturbed(Type, globalObject, callframe.this())) {
|
||||
return handleBodyAlreadyUsed(globalObject);
|
||||
@@ -1176,6 +1204,10 @@ pub fn Mixin(comptime Type: type) type {
|
||||
return handleBodyAlreadyUsed(globalObject);
|
||||
}
|
||||
|
||||
if (value.* == .HTMLBundle) {
|
||||
return globalObject.throwInvalidArguments("HTMLBundle cannot be used as a Response body", .{});
|
||||
}
|
||||
|
||||
if (value.* == .Locked) {
|
||||
if (value.Locked.action != .none or value.Locked.isDisturbed(Type, globalObject, callframe.this())) {
|
||||
return handleBodyAlreadyUsed(globalObject);
|
||||
@@ -1200,6 +1232,10 @@ pub fn Mixin(comptime Type: type) type {
|
||||
return handleBodyAlreadyUsed(globalObject);
|
||||
}
|
||||
|
||||
if (value.* == .HTMLBundle) {
|
||||
return globalObject.throwInvalidArguments("HTMLBundle cannot be used as a Response body", .{});
|
||||
}
|
||||
|
||||
if (value.* == .Locked) {
|
||||
if (value.Locked.action != .none or value.Locked.isDisturbed(Type, globalObject, callframe.this())) {
|
||||
return handleBodyAlreadyUsed(globalObject);
|
||||
@@ -1222,6 +1258,10 @@ pub fn Mixin(comptime Type: type) type {
|
||||
return handleBodyAlreadyUsed(globalObject);
|
||||
}
|
||||
|
||||
if (value.* == .HTMLBundle) {
|
||||
return globalObject.throwInvalidArguments("HTMLBundle cannot be used as a Response body", .{});
|
||||
}
|
||||
|
||||
if (value.* == .Locked) {
|
||||
if (value.Locked.action != .none or value.Locked.isDisturbed(Type, globalObject, callframe.this())) {
|
||||
return handleBodyAlreadyUsed(globalObject);
|
||||
@@ -1273,6 +1313,10 @@ pub fn Mixin(comptime Type: type) type {
|
||||
return handleBodyAlreadyUsed(globalObject);
|
||||
}
|
||||
|
||||
if (value.* == .HTMLBundle) {
|
||||
return globalObject.throwInvalidArguments("HTMLBundle cannot be used as a Response body", .{});
|
||||
}
|
||||
|
||||
if (value.* == .Locked) {
|
||||
if (value.Locked.action != .none or
|
||||
((this_value != .zero and value.Locked.isDisturbed(Type, globalObject, this_value)) or
|
||||
@@ -1414,6 +1458,9 @@ pub const ValueBufferer = struct {
|
||||
.Locked => {
|
||||
try sink.bufferLockedBodyValue(value);
|
||||
},
|
||||
.HTMLBundle => {
|
||||
return error.UnsupportedBodyType;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ pub export fn jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer(globa
|
||||
return any_blob.toArrayBufferTransfer(globalObject) catch return .zero;
|
||||
},
|
||||
.Error, .Locked => return .js_undefined,
|
||||
.HTMLBundle => return .js_undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -717,6 +717,13 @@ pub const WriteFileWaitFromLockedValueTask = struct {
|
||||
value.Locked.onReceiveValue = thenWrap;
|
||||
value.Locked.task = this;
|
||||
},
|
||||
.HTMLBundle => {
|
||||
file_blob.detach();
|
||||
_ = value.use();
|
||||
this.promise.deinit();
|
||||
bun.destroy(this);
|
||||
promise.reject(globalThis, ZigString.init("HTMLBundle cannot be written to a file").toErrorInstance(globalThis));
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
323
test/js/bun/http/bun-serve-html-response.test.ts
Normal file
323
test/js/bun/http/bun-serve-html-response.test.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
|
||||
test("static route with new Response(html) and custom headers/status", async () => {
|
||||
const dir = tempDirWithFiles("html-response-static", {
|
||||
"index.html": /*html*/ `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
<script type="module" src="./app.ts"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello from HTMLBundle</h1>
|
||||
</body>
|
||||
</html>`,
|
||||
"app.ts": /*ts*/ `console.log("App loaded");`,
|
||||
"server.ts": /*ts*/ `
|
||||
import html from "./index.html";
|
||||
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
development: false,
|
||||
routes: {
|
||||
"/": new Response(html, {
|
||||
status: 201,
|
||||
headers: {
|
||||
"X-Custom": "custom-value",
|
||||
"X-Test": "test-value"
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
console.log(server.port);
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "server.ts"],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const reader = proc.stdout.getReader();
|
||||
const { value } = await reader.read();
|
||||
const port = parseInt(new TextDecoder().decode(value).trim());
|
||||
|
||||
const response = await fetch(`http://localhost:${port}/`);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.headers.get("X-Custom")).toBe("custom-value");
|
||||
expect(response.headers.get("X-Test")).toBe("test-value");
|
||||
expect(response.headers.get("Content-Type")).toBe("text/html;charset=utf-8");
|
||||
|
||||
const text = await response.text();
|
||||
expect(text).toContain("Test Page");
|
||||
expect(text).toContain("Hello from HTMLBundle");
|
||||
expect(text).toMatch(/src="[^"]+\.js"/);
|
||||
|
||||
proc.kill();
|
||||
});
|
||||
|
||||
// todo: add build support for this
|
||||
test.each(["runtime" /*"build"*/])("many static routes with custom headers/status (%s)", async runtime => {
|
||||
const dir = tempDirWithFiles("html-response-static", {
|
||||
"index.html": /*html*/ `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
<script type="module" src="./app.ts"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello from HTMLBundle</h1>
|
||||
</body>
|
||||
</html>`,
|
||||
"hello.html": /*html*/ `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello from HTMLBundle</h1>
|
||||
</body>
|
||||
</html>`,
|
||||
"app.ts": /*ts*/ `console.log("App loaded");`,
|
||||
"server.ts": /*ts*/ `
|
||||
import html from "./index.html";
|
||||
import hello from "./hello.html";
|
||||
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
development: false,
|
||||
routes: {
|
||||
"/": new Response(html, {
|
||||
status: 201,
|
||||
headers: {
|
||||
"X-Custom": "custom-value",
|
||||
"X-Test": "test-value",
|
||||
}
|
||||
}),
|
||||
"/home": new Response(html),
|
||||
"/haha": new Response(html, {status: 400}),
|
||||
"/index.html": html,
|
||||
"/tea": {
|
||||
GET: new Response(html, {status: 418}),
|
||||
POST: () => new Response("Teapot!!!"),
|
||||
},
|
||||
"/hello": new Response(hello),
|
||||
"/*": new Response(html, {
|
||||
status: 404,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
console.log(server.port);
|
||||
`,
|
||||
});
|
||||
|
||||
let proc: Bun.Subprocess<"pipe", "pipe", "pipe">;
|
||||
if (runtime === "runtime") {
|
||||
proc = Bun.spawn({
|
||||
cmd: [bunExe(), "server.ts"],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
} else {
|
||||
const buildProc = Bun.spawn({
|
||||
cmd: [bunExe(), "build", "server.ts", "--outdir", "dist", "--target", "bun", "--splitting"],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
});
|
||||
await buildProc.exited;
|
||||
buildProc.kill();
|
||||
|
||||
proc = Bun.spawn({
|
||||
cmd: [bunExe(), "server.js"],
|
||||
env: bunEnv,
|
||||
cwd: dir + "/dist",
|
||||
stdout: "pipe",
|
||||
});
|
||||
}
|
||||
|
||||
const reader = proc.stdout.getReader();
|
||||
const { value } = await reader.read();
|
||||
const port = parseInt(new TextDecoder().decode(value).trim());
|
||||
|
||||
{
|
||||
const response = await fetch(`http://localhost:${port}/`);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.headers.get("X-Custom")).toBe("custom-value");
|
||||
expect(response.headers.get("X-Test")).toBe("test-value");
|
||||
expect(response.headers.get("Content-Type")).toBe("text/html;charset=utf-8");
|
||||
|
||||
const text = await response.text();
|
||||
expect(text).toContain("Test Page");
|
||||
expect(text).toContain("Hello from HTMLBundle");
|
||||
expect(text).toMatch(/src="[^"]+\.js"/);
|
||||
}
|
||||
|
||||
{
|
||||
const response = await fetch(`http://localhost:${port}/home`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("X-Custom")).not.toBe("custom-value");
|
||||
expect(response.headers.get("X-Test")).not.toBe("test-value");
|
||||
expect(response.headers.get("Content-Type")).toBe("text/html;charset=utf-8");
|
||||
|
||||
const text = await response.text();
|
||||
expect(text).toContain("Test Page");
|
||||
expect(text).toContain("Hello from HTMLBundle");
|
||||
expect(text).toMatch(/src="[^"]+\.js"/);
|
||||
}
|
||||
|
||||
{
|
||||
const response = await fetch(`http://localhost:${port}/index.html`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("X-Custom")).not.toBe("custom-value");
|
||||
expect(response.headers.get("X-Test")).not.toBe("test-value");
|
||||
expect(response.headers.get("Content-Type")).toBe("text/html;charset=utf-8");
|
||||
|
||||
const text = await response.text();
|
||||
expect(text).toContain("Test Page");
|
||||
expect(text).toContain("Hello from HTMLBundle");
|
||||
expect(text).toMatch(/src="[^"]+\.js"/);
|
||||
}
|
||||
|
||||
{
|
||||
const response = await fetch(`http://localhost:${port}/haha`);
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.headers.get("X-Custom")).not.toBe("custom-value");
|
||||
expect(response.headers.get("X-Test")).not.toBe("test-value");
|
||||
expect(response.headers.get("Content-Type")).toBe("text/html;charset=utf-8");
|
||||
|
||||
const text = await response.text();
|
||||
expect(text).toContain("Test Page");
|
||||
expect(text).toContain("Hello from HTMLBundle");
|
||||
expect(text).toMatch(/src="[^"]+\.js"/);
|
||||
}
|
||||
|
||||
{
|
||||
const response = await fetch(`http://localhost:${port}/tea`, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
expect(response.status).toBe(418);
|
||||
expect(response.headers.get("X-Custom")).not.toBe("custom-value");
|
||||
expect(response.headers.get("X-Test")).not.toBe("test-value");
|
||||
expect(response.headers.get("Content-Type")).toBe("text/html;charset=utf-8");
|
||||
|
||||
const text = await response.text();
|
||||
expect(text).toContain("Test Page");
|
||||
expect(text).toContain("Hello from HTMLBundle");
|
||||
expect(text).toMatch(/src="[^"]+\.js"/);
|
||||
}
|
||||
|
||||
{
|
||||
const response = await fetch(`http://localhost:${port}/tea`, {
|
||||
method: "POST",
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("X-Custom")).not.toBe("custom-value");
|
||||
expect(response.headers.get("X-Test")).not.toBe("test-value");
|
||||
expect(response.headers.get("Content-Type")).toBe("text/plain;charset=utf-8");
|
||||
|
||||
const text = await response.text();
|
||||
expect(text).toBe("Teapot!!!");
|
||||
}
|
||||
|
||||
{
|
||||
const response = await fetch(`http://localhost:${port}/hello`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("X-Custom")).not.toBe("custom-value");
|
||||
expect(response.headers.get("X-Test")).not.toBe("test-value");
|
||||
expect(response.headers.get("Content-Type")).toBe("text/html;charset=utf-8");
|
||||
|
||||
const text = await response.text();
|
||||
expect(text).toContain("Hello Page");
|
||||
expect(text).toContain("Hello from HTMLBundle");
|
||||
expect(text).toMatch(/src="[^"]+\.js"/);
|
||||
}
|
||||
|
||||
{
|
||||
const response = await fetch(`http://localhost:${port}/not-found`);
|
||||
expect(response.status).toBe(404);
|
||||
|
||||
expect(response.headers.get("X-Custom")).not.toBe("custom-value");
|
||||
expect(response.headers.get("X-Test")).not.toBe("test-value");
|
||||
expect(response.headers.get("Content-Type")).toBe("text/html;charset=utf-8");
|
||||
|
||||
const text = await response.text();
|
||||
expect(text).toContain("Test Page");
|
||||
expect(text).toContain("Hello from HTMLBundle");
|
||||
expect(text).toMatch(/src="[^"]+\.js"/);
|
||||
}
|
||||
|
||||
proc.kill();
|
||||
});
|
||||
|
||||
test("HTMLBundle in Response error conditions", async () => {
|
||||
const dir = tempDirWithFiles("html-response-errors", {
|
||||
"index.html": /*html*/ `<\!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Error Test</h1>
|
||||
</body>
|
||||
</html>`,
|
||||
"test.ts": `
|
||||
import html from "./index.html";
|
||||
const response = new Response(html);
|
||||
|
||||
const tests = [
|
||||
() => response.text(),
|
||||
() => response.blob(),
|
||||
() => response.json(),
|
||||
() => response.arrayBuffer(),
|
||||
() => response.formData(),
|
||||
() => Bun.write("output.html", response.body),
|
||||
() => Bun.spawn({
|
||||
cmd: ["echo", "test"],
|
||||
stdin: response.body
|
||||
})
|
||||
];
|
||||
|
||||
for (let i = 0; i < tests.length; i++) {
|
||||
try {
|
||||
const result = await tests[i]();
|
||||
console.log(\`FAIL: Test \${i} should have thrown\`);
|
||||
} catch (e) {
|
||||
if (e.toString().includes("HTMLBundle")) {
|
||||
console.log(\`PASS: Test \${i} threw as expected\`);
|
||||
} else {
|
||||
console.log(\`HALF PASS: Test \${i} should have thrown better error message\`);
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.ts"],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
expect(await proc.stdout.text()).toMatchInlineSnapshot(`
|
||||
"PASS: Test 0 threw as expected
|
||||
PASS: Test 1 threw as expected
|
||||
PASS: Test 2 threw as expected
|
||||
PASS: Test 3 threw as expected
|
||||
PASS: Test 4 threw as expected
|
||||
PASS: Test 5 threw as expected
|
||||
PASS: Test 6 threw as expected
|
||||
"
|
||||
`);
|
||||
});
|
||||
Reference in New Issue
Block a user