Skip to content

bun-lambda: print a console string as it is and every other value with Bun.inspect - #35441

Open
robobun wants to merge 2 commits into
mainfrom
farm/cee5b67a/bun-lambda-console-format
Open

robobun wants to merge 2 commits into
mainfrom
farm/cee5b67a/bun-lambda-console-format

Conversation

@robobun

@robobun robobun commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #4826

Problem

  • Under bun-lambda, console.log("url:", url) prints "url:" "https://…". console.log("GET", "/caf%c3%a9", 200) prints "GET" "/caf3%a9". The error endpoints get errorMessage: "\"Function timed out\"".
  • log() (packages/bun-lambda/runtime.ts:25) prints each argument with Bun.inspect, which quotes a string, and the native console formats the pieces again. formatError() (:114) does the same.

Fix

  • formatArgs() builds the message once: a string as it is, every other value with Bun.inspect. formatError() uses the same rule.
  • For a format string, formatSpecifier() converts %s, %d, %i, %f, %j as node's util.format does. Under %s an object with a built-in toString (an Error, a Date) is inspected, so an Error keeps its stack. %j prints a cycle as [Circular]. Bun.inspect prints %o, %O.
  • Verified: test/integration/bun-lambda/bun-lambda.test.ts (4 of 6 fail on main). Same text on 16 binaries, 0.7.3 to 1.4.3.
  • Self-reviewed: 17 concerns raised, 17 addressed. One needs an edit to console: only a primitive-string first argument is a format string #45571. Review round two: 3 concerns, 2 addressed (%s on an Error, the catch in formatArgs()), 1 left (console.log() with no arguments is a pre-existing drop outside this fix).

Background

  • A released bun runs runtime.ts. One stdout line is one CloudWatch record, so log() writes line breaks in a message as \r.
  • Considered node:util's format (the earlier version). It prints Headers {}, logs the event body with a Request, and loads 17 M instructions at cold start or in the first format-string log.
  • Land this PR before console: only a primitive-string first argument is a format string #45571, which copied the earlier log() and drops it with its bun-lambda test. These tests pass on its native change.

Downsides

  • Log lines with a string change: CloudWatch filters on the quoted text stop matching.
  • A BigInt under %j, or a toString that throws under %s, now throws from the console call, as util.format does on node's runtime. On main the value printed as Bun.inspect text. In a handler the throw becomes a 500 response and an ERROR record.
  • Cold start: +503 K instructions:u on 13.16 M (+3.8%, A/A +12 K) for 2.3 KB more source, measured with aws4fetch on disk. A real layer installs aws4fetch first (Bun layer for AWS Lambda: "error: Cannot find package 'aws4fetch' from '/opt/runtime.ts'" #19756).
Notes

Output (the real runtime.ts against a mock Lambda Runtime API, bun 1.4.3. ⏎ is \r.)

handler call main this PR
console.log("url:", request.url) "url:" "http://api.example.com/hello" url: http://api.example.com/hello
console.error("payload:", JSON.stringify({ a: 1 }, null, 2)) "payload:" "{\n \"a\": 1\n}" payload: {⏎ "a": 1⏎}
console.log("hi %s, you are %d", "bob", 42) "hi "bob", you are 42" hi bob, you are 42
console.log("GET", "/caf%c3%a9", 200) "GET" "/caf3%a9" GET /caf%c3%a9 200
console.log({ url: "/caf%c3%a9" }, 200) {⏎ url: "/caf3%a9",⏎} {⏎ url: "/caf%c3%a9",⏎} 200
console.log("headers:", new Headers({ a: "1" })) "headers:" Headers {⏎ "a": "1",⏎} headers: Headers {⏎ "a": "1",⏎}
console.log("req %s took %dms", "GET /", 12, headers) "req "GET /" took 12ms" Headers {…} req GET / took 12ms Headers {⏎ "a": "1",⏎}
console.log("Event: %j", { c: [1] }) "Event: "{\r c: [ 1 ],\r}"" Event: {"c":[1]}
runtime diagnostic at exit ERROR "Runtime failed to send request to Lambda [status: 500]" ERROR Runtime failed to send request to Lambda [status: 500]
errorMessage after a timeout "\"Function timed out\"" "Function timed out"
  • A value that is not a string prints as on main: a Response, a Blob, a 5-level object, Headers, a Request, an Error, the IAM policy in the runtime's own WebSocket warning.
  • A cause that is not an Error and not a string keeps its Bun.inspect text in errorMessage. errorType stays "Error".

The format string

  • Only a string first argument is a format string, and only when more arguments follow. That is the rule of node and of the console standard.
  • For a string, a number, a BigInt, a boolean, a Symbol, null and undefined, every argument list prints what util.format prints (node v26.3.0 and Bun's port), and %j with a BigInt throws a TypeError as it does there.
  • For an object: %d, %i, %f and %j convert as util.format does. %s follows node's hasBuiltInToString rule: the owner of toString or Symbol.toPrimitive in the prototype chain is found, and when its constructor is a global built-in the value prints with Bun.inspect (an Error with its stack, a Date as ISO text, { a: 1 }). An object with its own toString or Symbol.toPrimitive prints String(value). Node uses depth: 0 for the inspect under %s. This PR uses the default depth, the same as %o. %o, %O and the arguments after the last specifier print with Bun.inspect.
  • %j on a cycle prints [Circular]. The cyclic TypeError is told apart by its message, computed once from a known cycle, as util.format does. Every other throw (a BigInt under %j, a user toString that throws) reaches the caller. A throw in a handler's fetch is a 500 response and an ERROR record with the stack.

Why not node:util

The earlier version of this PR sent every call to util.format, and #45571 has a copy of that log(). It is what node's Lambda runtime does (aws-lambda-nodejs-runtime-interface-client, src/LogPatch.js: util.format(message, ...params), then \n to \r). In Bun it changes the printer of every value, and a Lambda handler logs web objects:

  • console.log("headers:", request.headers) prints headers: Headers {}. A Response, a Blob and a FormData print empty too.
  • console.log(request) prints Request { aws: { ..., body: '...' } }. The runtime stores the event in request.aws, and util.inspect lists own properties. So the request body lands in CloudWatch. main and this PR print Request (N bytes) { method, url, headers }.
  • The IAM policy in the runtime's own WebSocket warning prints Action: [Array], Resource: [Array], because util.inspect stops at depth 2.
  • A static node:util import loads it in every execution environment: 13.16 M to 30.33 M instructions to the first GET /invocation/next, 13.9 to 22.4 ms, 2,301 to 5,042 heap objects, for a handler that never logs.
  • util.inspect: print the state of Response, Request, Headers, Blob and six more classes #45355 makes util.inspect print the web classes. It is open. It does not change the depth or the load.

A second version kept Bun.inspect for values and loaded node:util only at the first format string, with each object in a wrapper that sent node:util back to Bun.inspect. Then the load ran inside the first invocation that logs a format string: 17.96 M instructions and 2,698 heap objects (417 KB), where main runs none. The switch in formatSpecifier() runs 0.2 M there and loads nothing.

The hand-off to the console

  • log() keeps main's call shape: the level, the request id, then one finished message where main spread the pieces. The message is the last argument, so every release prints it as it is.
  • One joined string is two write(2) calls per line on bun 1.1.22 and older (the text, then the newline). This shape is one call on all 10 binaries measured, 0.7.3 to 1.4.3, as on main. It also does not copy a long message: a joined string cost 300 more instructions than main for a logged Error.
  • A CRLF in a string folds to one \r. node's runtime writes \r\r. On main a raw CRLF does not reach the fold, because Bun.inspect escapes it.

Releases

The layer runs on the release that the user picks (build-layer --release). 54 handler calls ran on 16 binaries: 0.7.3, 0.8.0, 0.8.1, 1.0.0, 1.0.4, 1.1.0, 1.1.22, 1.1.23, 1.1.45, 1.2.0, 1.2.23, 1.3.0, 1.3.14, 1.4.0, 1.4.3 and a 1.4.3 canary. 50 calls print the same text on all 16. The other 4 differ only by Bun.inspect's own text in that release: the field order of a Response, the text of an Error, the label of a null-prototype object, and a string under %o on 0.7.3.

Measurements (bun 1.4.3 release, linux x64 baseline build, transpiler cache off, the test layout with aws4fetch on disk. Instructions are instructions:u from perf_event_open over the process tree.)

  • Cold start to the first GET /invocation/next, handler never logs, median of 40 interleaved runs: main 13,158,521, this PR +502,958 (+3.8%), a second copy of main +12,094. Wall time 12.65 and 12.89 ms (second copy of main 13.50). Peak RSS 19,824 and 19,880 KB.
  • Heap objects at init: main 2,301, this PR 2,305. After the first format-string log: main 3,062, this PR 3,109 (+15 KB).
  • The first format-string log of a process: +0.2 M instructions (main +0.1 M, the spread of 11 runs is 0.5 M).
  • Per call, instructions, main to this PR: log("request received") 8,721 to 5,560. log("url:", url) 12,071 to 5,685. log(obj) 12,715 to 12,497. log("obj:", obj) 15,198 to 12,806. log(headers) 55,660 to 55,367. log(request) 81,787 to 81,375. error(new Error) 45,244 to 44,977. ("hi %s, you are %d", "bob", 42) 13,668 to 7,537. ("req %s took %dms", "GET /", 12, headers) 64,938 to 59,391. ("Event: %j", event) 45,757 to 8,905. Two copies of main differ by up to 295.
  • GC cells per call (1.4.3 canary, heapStats() deltas, 3 runs): fewer than main on 15 of 15 shapes. For example log(obj) 9 to 6, log(headers) 14 to 11, ("hi %s, you are %d", "bob", 42) 12 to 8.
  • Bun.inspect calls per console.log("url:", url): 0 (main 2). Regex passes per call: 1 (main 1 per argument). Console arguments per call: 3 (main 2 plus 1 per argument).
  • write(2) calls per log line: 1 (main 1).
  • No native change: 0 host functions, 0 binary bytes. runtime.ts is 24,584 to 26,850 bytes.

Cold start in a real layer

Tests

  • console prints a string as it is and every other value as Bun.inspect does: 29 console calls in one handler, at init and in an invocation, through the six methods. The expected text of each value is Bun.inspect of the same value in the test process. The Request row compares with Bun.inspect(request) from the handler. The last assertion reads a heap marker: the runtime has not loaded node:util after all the calls.
  • errorMessage of ... is the cause as console prints it: a string at init, an object at init, and Function timed out in an invocation. The test checks the error body, stdout and the exit code.
  • a BigInt under %j throws in the handler, as any other throw in fetch: the ERROR record holds the TypeError with its stack and the invocation posts a 500 response.
  • With main's runtime.ts, 4 of 6 fail. With the earlier util.format version, 2 of 5 fail. With console: only a primitive-string first argument is a format string #45571's copy of log(), 4 of 5 fail.
  • 39 of 41 mutants of the new code fail a test. The other two print the same bytes: one removes the includes("%") check that skips the specifier pass, one gives the console a single joined string.
  • The change is TypeScript in packages/, so a released bun runs it too. The tests ran on the debug build and on 1.4.3.

Landing order with #45571

Not changed

  • The line layout: LEVEL message before the first invocation, LEVEL RequestId: ID message in one.
  • console.dir, table, assert, group, count and time are not wrapped.
  • errorType is "Error" for a cause that is not an Error. node's runtime sends typeof.
  • A string reaches the log as it is, as in node's runtime. A \n in it becomes \r inside the same record.

[review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/integration/bun-lambda/bun-lambda.test.ts
bun test v1.4.0 (1e36aad1d)

test/integration/bun-lambda/bun-lambda.test.ts:
287 |   const exitedPromise = proc.exited;
288 |   const { pathname, body } = await initError;
289 |   const [stdout, stderr, exitCode] = await Promise.all([stdoutPromise, stderrPromise, exitedPromise]);
290 | 
291 |   expect(stderr).toBe("");
292 |   expect(stdout).toBe("ERROR handler does not have a default export with a function named 'fetch'\n");
                       ^
error: expect(received).toBe(expected)

- "ERROR handler does not have a default export with a function named 'fetch'
+ "ERROR "handler does not have a default export with a function named 'fetch'"
  "

- Expected  - 1
+ Received  + 1

      at <anonymous> (/workspace/bun/test/integration/bun-lambda/bun-lambda.test.ts:292:18)
(fail) init error errorMessage is not wrapped in extra quotes [783.88ms]
230 |   expect(stderr).toBe("");
231 |   expect(result.unexpected).toBeUndefined();
232 |   // Lines from inside the invocation carry the RequestId 
... (truncated)

release without fix: 2 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/integration/bun-lambda/bun-lambda.test.ts:
230 |   expect(stderr).toBe("");
231 |   expect(result.unexpected).toBeUndefined();
232 |   // Lines from inside the invocation carry the RequestId prefix; the trailing
233 |   // ERROR line is the runtime shutting down after the mock server returns 500.
234 |   const lines = stdout.split("\n").filter(line => line.includes("RequestId: req-log"));
235 |   expect(lines).toEqual([
                      ^
error: expect(received).toEqual(expected)

  [
-   "INFO RequestId: req-log url: http://api.example.com/hello",
-   "INFO RequestId: req-log hi bob, you are 42",
+   "INFO RequestId: req-log "url:" "http://api.example.com/hello"",
+   "INFO RequestId: req-log "hi "bob", you are 42"",
+   "ERROR RequestId: req-log "payload:" "{\n  \"a\": 1\n}"",
    
- "ERROR RequestId: req-log payload: {
-   "a": 1
+ "INFO RequestId: req-log "obj:" {
+   a: 1,
  }"
  ,
-   "INFO RequestId: req-log obj: { a: 1 }",
-   "WARN RequestId: req-log keep %s literal",
-   
- "INFO RequestId: req-log crlf: a
- b"
- ,
+   "WARN RequestId: req-log "keep %s literal"",
+   "INFO RequestId: req-log "crlf:" "a\r\nb""
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/integration/bun-lambda/bun-lambda.test.ts
bun test v1.4.0 (1e36aad1d)

test/integration/bun-lambda/bun-lambda.test.ts:
(pass) init error errorMessage is not wrapped in extra quotes [1187.27ms]
(pass) console.log formats arguments like Node, not Bun.inspect per-arg [1401.61ms]
(pass) lambda HTTP events cannot override the request authority [1567.24ms]

 3 pass
 0 fail
 14 expect() calls
Ran 3 tests across 1 file. [4.36s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     1e36aad1da
  features     baseline

22 deps, 108 codegen, 1170 objects in 1163ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1233] install /workspace/bun
bun install v1.4.0-canary.1 (1498d7b77)

Checked 124 installs across 170 packages (no changes) [34.00ms]
[2/1233] install /workspace/bun/packages/bun-error
bun install v1.4.0-canary.1 (1498d7b77)

Checked 1 install across 2 packages (no changes) [2.00ms]
[3/1233] gen bindgenv2
[4/1233] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (1498d7b77)

Checked 129 installs across 147 packages (no changes) [9.00ms]
[5/1233] gen ErrorCode+*.h
[6/1233] fetch zlib
[zlib] up to date
[7/1233] fetch picohttpparser
[picohttpparser] up to date
[8/1233] fetch tinycc
[tinycc] up to date
[9/1233] gen .bind.ts → GeneratedBindings.cpp
[10/1233] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[11/1233] subst deps/zlib/zlib.h
[12/1233] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/Pr
... (truncated)
diff hotspot
packages/bun-lambda/runtime.ts                 |   9 +-
 test/integration/bun-lambda/bun-lambda.test.ts | 169 ++++++++++++++++++++++++-
 2 files changed, 173 insertions(+), 5 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                            reads  edits  tests
packages/bun-lambda/runtime.ts                      9      8      0
test/integration/bun-lambda/bun-lambda.test.ts      2      6      0

@robobun

robobun commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:26 AM PT - Oct 21st, 2026

❌ @robobun, your commit 411f34f has 1 failures in Build #130198 (All Failures):

  • step failed outside runner - exit 1 on ⚙️

🧪   To try this PR locally:

bunx bun-pr 35441

That installs a local version of the PR into your bun-35441 executable, so you can run:

bun-35441 --bun

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 2a06b593-db02-491a-8988-9cc0f8160d2f
📥 Commits

Reviewing files that changed from the base of the PR and between 30a44dd and f2ae540.

📒 Files selected for processing: 2
  • packages/bun-lambda/runtime.ts
  • test/integration/bun-lambda/bun-lambda.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

Walkthrough

The Lambda runtime now formats console arguments with custom string-preserving handling for format specifiers, special values, and fallback inspection. It normalizes newlines and formats non-Error Lambda messages consistently. Integration tests cover logging, errors, timeouts, and runtime execution.

Changes

Lambda console formatting

Layer / File(s) Summary
Runtime value and specifier formatting
packages/bun-lambda/runtime.ts
The runtime formats console values and supports %s, %d, %i, %f, %j, %c, and %%, including unmatched arguments and formatting fallbacks.
Runtime logging and Lambda error output
packages/bun-lambda/runtime.ts
The logger joins formatted arguments, normalizes \r\n and \n to \r, keeps the request ID separate, and formats non-Error Lambda messages with the same value formatter.
Integration validation
test/integration/bun-lambda/bun-lambda.test.ts
Tests add shared Lambda execution infrastructure and validate console output, responses, errors, timeouts, exit status, and deferred node:util loading.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to f2ae5

The Lambda formatting change is ready to merge after normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Linked Issues check Passed Issue [#4826] requires correct console.log formatting in the Lambda runtime. runtime.ts now preserves string arguments, applies format specifiers once, formats other values with Bun.inspect, and…
Out of Scope Changes check Passed The changes remain in the Lambda runtime and its integration test. The formatError() change removes the same string-quoting defect from non-Error Lambda diagnostics, and its tests verify the relat…
Title check Passed The title clearly identifies the bun-lambda logging change and describes the intended string-preserving and Bun.inspect behavior.
Description check Passed The description clearly explains the problem, implementation, scope, trade-offs, compatibility considerations, and verification results. It includes detailed testing evidence and build results, which …
Full details: Linked Issues check

Explanation

Issue [#4826] requires correct console.log formatting in the Lambda runtime. runtime.ts now preserves string arguments, applies format specifiers once, formats other values with Bun.inspect, and folds line breaks for CloudWatch records. The integration test covers comma-separated URLs, concatenated strings, objects, printf specifiers, multiline values, CRLF values, and all log levels. The reported test run passes.

Full details: Out of Scope Changes check

Explanation

The changes remain in the Lambda runtime and its integration test. The formatError() change removes the same string-quoting defect from non-Error Lambda diagnostics, and its tests verify the related endpoint output. The test harness and concurrency change support the runtime behavior under test. No unrelated product area changed.

Full details: Description check

Explanation

The description clearly explains the problem, implementation, scope, trade-offs, compatibility considerations, and verification results. It includes detailed testing evidence and build results, which covers the template requirements despite using expanded headings instead of the exact template headings.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/bun-lambda/runtime.ts`:
- Line 26: Update the message normalization in the format call to treat CRLF as
a single line break before converting standalone LF characters, preserving
existing behavior for inputs containing only LF or CR.

In `@test/integration/bun-lambda/bun-lambda.test.ts`:
- Around line 149-153: Add the missing console-formatting cases to the
integration test: invoke info, debug, and trace; exercise %j substitution; log a
URL formed by concatenating parts; and include a message containing CRLF.
Preserve the existing log, error, warn, %s/%d, and separate-URL coverage in the
same test scenario.
- Around line 205-220: Update the Bun.spawn flow in this integration test to
begin draining proc.stdout, proc.stderr, and proc.exited immediately after
spawning, before awaiting responded. Store the three concurrent promises and
await responded only afterward, preserving the existing output and exit
collection behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 19ac9c44-b861-491a-96fa-a66b8957873a

📥 Commits

Reviewing files that changed from the base of the PR and between 028f7a3 and 30a44dd.

📒 Files selected for processing (2)
  • packages/bun-lambda/runtime.ts
  • test/integration/bun-lambda/bun-lambda.test.ts

Comment thread packages/bun-lambda/runtime.ts Outdated
Comment thread test/integration/bun-lambda/bun-lambda.test.ts Outdated
Comment thread test/integration/bun-lambda/bun-lambda.test.ts Outdated
@robobun

robobun commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: reworked and rebased on main. The PR no longer uses util.format.

How to reproduce the problem

Run bun bd test test/integration/bun-lambda/bun-lambda.test.ts with main's packages/bun-lambda/runtime.ts. 3 of 5 tests fail. The tests run the real runtime.ts against a mock Lambda Runtime API. A handler that calls console.log("url:", request.url) prints INFO RequestId: req-log "url:" "http://api.example.com/hello".

Why the rework

The earlier version sent every console call to util.format. With the same mock:

  • console.log("headers:", request.headers) printed headers: Headers {}.
  • console.log(request) printed the event with the request body, because the runtime stores the event in request.aws.
  • The node:util import added 17 M instructions to each cold start.

Now a string prints as it is, Bun.inspect prints every other value as before, and a switch in runtime.ts handles the format specifiers. The description has the measurements.

Order with #45571

#45571 has a copy of the earlier log(). This PR has to merge first. Then #45571 drops its packages/bun-lambda/runtime.ts hunk, its bun-lambda test and its Fixes #4826 line. With the native change of #45571 cherry-picked on top of this PR, the 5 tests pass.

…h Bun.inspect

log() printed each argument with Bun.inspect and gave the pieces to the
native console. Bun.inspect quotes and escapes a string, and the console
then ran its format pass over text that was already printed. In
CloudWatch, console.log("url:", request.url) printed "url:" "https://...",
and console.log("GET", "/caf%c3%a9", 200) printed "GET" "/caf3%a9".
formatError() made the same Bun.inspect call, so the runtime's own string
diagnostics reached the Lambda error endpoints inside extra quotes.

formatArgs() now builds the message. A string prints as it is, and
Bun.inspect prints every other value, as before. A string first argument
is a format string: formatSpecifier() converts each parameter as node's
util.format does, and Bun.inspect prints an object. node:util itself is
not loaded. log() gives the console the level and one finished string.
formatError() sends a string cause as it is.
@robobun
robobun force-pushed the farm/cee5b67a/bun-lambda-console-format branch from 1e36aad to f2ae540 Compare October 20, 2026 03:20
@robobun robobun changed the title bun-lambda: format console.log args with util.format instead of Bun.inspect per-arg bun-lambda: print a console string as it is and every other value with Bun.inspect Oct 20, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked the runtime's own diagnostics that now pass through the new formatter (the exit(...) and warnOnce(...) messages in packages/bun-lambda/runtime.ts, e.g. lines 135, 149, 266-346 and 769-796): none of their first-argument strings contain %, so the specifier path cannot consume or drop their trailing args (the IAM policy object, the caught error). The %c3-style percent-encoded URL case is consumed as %c only when the URL is in the first argument, which matches util.format, and the test helper's using dir/using server/await using proc are scoped inside runLambda so they are released before any assertion runs.

Extended reasoning...

The change replaces per-argument Bun.inspect in the bun-lambda console override with a hand-rolled printf-style formatter and switches formatError's non-Error branch to pass strings through unquoted; it touches no auth, crypto or permission surface, only CloudWatch log text and the Lambda error payload. Three inline findings were confirmed (Error under %s losing its stack, the catch-to-Bun.inspect fallback, and the no-args early return), so a human should weigh those; the additional checks here on the runtime's own log call sites and the test's resource scoping turned up nothing further.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread packages/bun-lambda/runtime.ts Outdated
Comment thread packages/bun-lambda/runtime.ts Outdated
Comment thread packages/bun-lambda/runtime.ts
… a cycle as [Circular]

An Error under %s keeps its stack. A BigInt under %j throws, as node:util does.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment on lines +51 to +52
const constructor = Object.getOwnPropertyDescriptor(owner, "constructor")?.value;
return typeof constructor === "function" && (globalThis as any)[constructor.name] === constructor;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) Lambda handlers that log a Buffer, URL or URLSearchParams with %s get the inspected structure in CloudWatch, where Node's runtime prints the text. hasBuiltInToString at packages/bun-lambda/runtime.ts:52 treats any constructor found on globalThis as built-in. Node's rule covers only ECMAScript intrinsics (the list at src/js/internal/util/inspect.js:203), so console.log("GET %s", url) prints the multi-line URL struct instead of the href and a Buffer prints its byte list. Fix: classify by Node's fixed intrinsic-name set rather than globalThis identity, so Buffer, URL, URLSearchParams and their subclasses go through String(value) while Error, Date and plain objects still inspect.

Why this was flagged

A handler calls console.log("fetching %s", new URL(request.url)) or console.log("body: %s", Buffer.from(text)); console.log is overridden at packages/bun-lambda/runtime.ts:148 and the %s arm at runtime.ts:87 calls hasBuiltInToString. The walk stops at URL.prototype, which owns toString, reads its constructor, and runtime.ts:52 finds globalThis.URL === URL, so it returns true and the value goes to Bun.inspect, the 14-line struct shown at test/js/web/url/url.test.ts:256, folded with \r into one record; a Buffer prints as its numeric byte list. Node's util.format and Bun's port at src/js/internal/util/inspect.js:2556-2558 test the constructor name against a fixed intrinsic set (inspect.js:203) that has no Buffer, URL or URLSearchParams, so they print String(value). The base branch inspected every argument, so output is not worse than base, but the comment at runtime.ts:30 and the PR state this is node:util's rule.

Verification: Triggered whenever a handler logs a URL, URLSearchParams or Buffer under %s. packages/bun-lambda/runtime.ts:52 decides "built-in" by (globalThis as any)[constructor.name] === constructor, so the %s arm at runtime.ts:87-88 calls Bun.inspect(value). Node's rule at src/js/internal/util/inspect.js:2556-2559 checks builtInObjects.has(descriptor.value.name), where URL/Buffer/URLSearchParams are absent.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix console.log in Lambda runtime

1 participant