Skip to content

fix(timeline): headers info in response timeline#8766

Open
shubh-bruno wants to merge 2 commits into
usebruno:mainfrom
shubh-bruno:fix/headers-info
Open

fix(timeline): headers info in response timeline#8766
shubh-bruno wants to merge 2 commits into
usebruno:mainfrom
shubh-bruno:fix/headers-info

Conversation

@shubh-bruno

@shubh-bruno shubh-bruno commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Description

Contribution Checklist:

  • I've used AI significantly to create this pull request
  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.

Publishing to New Package Managers

Please see here for more information.

Summary by CodeRabbit

  • New Features

    • Enhanced request and response header displays with clearer ordering, formatting, and source information.
    • Network timelines now show headers in request order, including transport-level headers.
    • Header counts are displayed directly in the expandable headers view.
    • Request data and header values are rendered more reliably, including structured values.
  • Bug Fixes

    • Fixed missing request headers in network timelines.
    • Improved handling of headers modified by pre-request scripts.
    • Added support for setting request headers from arrays.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds shared header-source resolution, captures transport and pre-request-script header provenance, and applies ordered header rendering across request details, request timeline, network logs, and header tables.

Changes

Header provenance and timeline rendering

Layer / File(s) Summary
Shared header resolution contract
packages/bruno-common/src/utils/headers-source.js, packages/bruno-common/src/utils/index.ts
Normalizes configured and timeline headers, assigns source priority, stringifies values, deduplicates names, and exports the shared utilities.
Request execution header capture
packages/bruno-electron/src/ipc/network/axios-instance.js, packages/bruno-electron/src/ipc/network/index.js, packages/bruno-js/src/bruno-request.js
Serializes and backfills sent transport headers, reports headers changed by pre-request scripts, and supports array-form header updates.
Timeline and request detail rendering
packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/index.js, packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/...
Passes request context into tabs, renders resolved header rows, reorders network header logs, displays header counts, and adjusts table sizing.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PreRequest
  participant AxiosNetwork
  participant ClientRequest
  participant HeaderResolver
  participant BrunoApp
  PreRequest->>PreRequest: identify scriptSetHeaders
  AxiosNetwork->>ClientRequest: execute request
  ClientRequest-->>AxiosNetwork: expose sent headers
  AxiosNetwork->>HeaderResolver: provide timeline and request headers
  HeaderResolver-->>BrunoApp: return ordered header rows
  BrunoApp-->>BrunoApp: render request and network headers
Loading

Possibly related PRs

  • usebruno/bruno#8741: Overlaps with the timeline header refactor and shared header-resolution utilities.

Suggested reviewers: bijin-bruno

Poem

Headers march in ordered lines,
From scripts, requests, transport signs.
Timelines fill each missing space,
Tables show them all in place.
A tidy trail from wire to view.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing header information in the response timeline.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/headers-info

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/bruno-electron/src/ipc/network/index.js (1)

1792-1799: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Runner path's requestSent is missing scriptSetHeaders.

The standalone runRequest path includes scriptSetHeaders: request.scriptSetHeaders || [] in its requestSent payload (line 992), but this Runner-flow requestSent (built from the same runPreRequest-populated request.scriptSetHeaders) omits it. Header-provenance attribution for pre-request-script-set headers will silently break for Collection Runner runs.

🐛 Proposed fix
             let requestSent = {
               url: request.url,
               method: request.method,
               headers: headersSent,
+              scriptSetHeaders: request.scriptSetHeaders || [],
               data: requestData,
               dataBuffer: requestDataBuffer,
               timestamp: Date.now()
             };
🤖 Prompt for 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.

In `@packages/bruno-electron/src/ipc/network/index.js` around lines 1792 - 1799,
Update the Runner-flow requestSent payload in the request execution path to
include scriptSetHeaders from request, defaulting to an empty array as in the
standalone runRequest payload. Preserve the existing requestSent fields and use
the runPreRequest-populated request.scriptSetHeaders value.
🧹 Nitpick comments (1)
packages/bruno-common/src/utils/headers-source.js (1)

1-108: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add the utility with the package TypeScript extension.

packages/bruno-common already has real TypeScript runtime source, tsconfig.json, and @rollup/plugin-typescript, so this new utility should be headers-source.ts with appropriate typed inputs/outputs instead of headers-source.js.

🤖 Prompt for 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.

In `@packages/bruno-common/src/utils/headers-source.js` around lines 1 - 108, The
new headers utility should use the TypeScript extension and typed interfaces
throughout. Rename the implementation to headers-source.ts, add appropriate
types for header entries, timeline entries, request data, collection/item
inputs, and buildHeaderRows inputs/outputs, and ensure the existing behavior
remains unchanged.

Source: Path instructions

🤖 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/bruno-electron/src/ipc/network/axios-instance.js`:
- Around line 234-273: Make redirect timelines hop-aware in backfillSentHeaders
at packages/bruno-electron/src/ipc/network/axios-instance.js:234-273 by
resetting the existing-header set and insertion boundary at the most recent
request marker, so only the current hop is scanned. Update sortRequestHeaderLogs
at
packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js:11-40
to split slots at each request entry and sort/reassign request headers
independently within each hop segment.
- Around line 238-255: Update parseSentHeaders to stop reading the private
ClientRequest._header field. Use an API-supported source or mechanism that
preserves the sent wire-header names and ordering for timeline backfill, while
retaining the existing fallback behavior only if it provides equivalent
wire-header data; do not rely on getHeaders() as a casing/order substitute.

In `@packages/bruno-js/src/bruno-request.js`:
- Around line 140-153: Update setHeaders in the array-form merge path to filter
out prototype-pollution keys (__proto__, constructor, and prototype) from each
script-supplied entry before merging into this.req.headers. Preserve merging of
all other valid header keys and the existing object-form replacement behavior.

---

Outside diff comments:
In `@packages/bruno-electron/src/ipc/network/index.js`:
- Around line 1792-1799: Update the Runner-flow requestSent payload in the
request execution path to include scriptSetHeaders from request, defaulting to
an empty array as in the standalone runRequest payload. Preserve the existing
requestSent fields and use the runPreRequest-populated request.scriptSetHeaders
value.

---

Nitpick comments:
In `@packages/bruno-common/src/utils/headers-source.js`:
- Around line 1-108: The new headers utility should use the TypeScript extension
and typed interfaces throughout. Rename the implementation to headers-source.ts,
add appropriate types for header entries, timeline entries, request data,
collection/item inputs, and buildHeaderRows inputs/outputs, and ensure the
existing behavior remains unchanged.
🪄 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: CHILL

Plan: Pro Plus

Run ID: b516d0ab-9d59-43cc-9fda-77749788f80e

📥 Commits

Reviewing files that changed from the base of the PR and between 3c5b568 and 76fecd3.

📒 Files selected for processing (11)
  • packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/index.js
  • packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Headers/index.js
  • packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js
  • packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Request/index.js
  • packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/StyledWrapper.js
  • packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/index.js
  • packages/bruno-common/src/utils/headers-source.js
  • packages/bruno-common/src/utils/index.ts
  • packages/bruno-electron/src/ipc/network/axios-instance.js
  • packages/bruno-electron/src/ipc/network/index.js
  • packages/bruno-js/src/bruno-request.js

Comment on lines +234 to +273
// The Node http adapter appends transport headers (Host, Connection, Accept-Encoding,
// Content-Length) after the request interceptor logged config.headers, so they're missing from the
// timeline. Read the real serialized header block off the ClientRequest and backfill the ones we
// didn't already log, inserted right after the existing request-header block so ordering holds.
const parseSentHeaders = (req) => {
const raw = req?._header;
if (typeof raw === 'string') {
return raw
.split('\r\n')
.slice(1) // drop the request line (e.g. "GET /path HTTP/1.1")
.filter(Boolean)
.map((line) => {
const idx = line.indexOf(':');
return idx === -1 ? null : { name: line.slice(0, idx).trim(), value: line.slice(idx + 1).trim() };
})
.filter((h) => h && h.name);
}
const hdrs = typeof req?.getHeaders === 'function' ? req.getHeaders() : null;
return hdrs
? Object.entries(hdrs).map(([name, value]) => ({ name, value: Array.isArray(value) ? value.join(', ') : String(value) }))
: [];
};

const backfillSentHeaders = (timeline, req) => {
if (!Array.isArray(timeline) || !req) return;
const existing = new Set();
let lastIdx = -1;
timeline.forEach((entry, i) => {
if (entry?.type !== 'requestHeader' || typeof entry.message !== 'string') return;
const idx = entry.message.indexOf(':');
if (idx !== -1) existing.add(entry.message.slice(0, idx).trim().toLowerCase());
lastIdx = i;
});
const additions = parseSentHeaders(req)
.filter((h) => !existing.has(h.name.toLowerCase()))
.map((h) => ({ timestamp: new Date(), type: 'requestHeader', message: `${h.name}: ${h.value}` }));
if (!additions.length) return;
timeline.splice(lastIdx >= 0 ? lastIdx + 1 : timeline.length, 0, ...additions);
};

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Redirects share one timeline/logs array across hops; new header logic in both files isn't hop-aware. requestConfig.metadata.timeline = timeline (axios-instance.js, pre-existing) reuses the same array for every redirect hop of a request. Neither backfillSentHeaders nor sortRequestHeaderLogs segments by hop, so both misbehave specifically on redirected requests.

  • packages/bruno-electron/src/ipc/network/axios-instance.js#L234-L273: scope the existing/lastIdx scan in backfillSentHeaders to entries after the most recent type === 'request' marker (the start of the current hop), not the whole array.
  • packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js#L11-L40: segment slots by hop boundary (reset at each type === 'request' entry) in sortRequestHeaderLogs, and sort/reassign within each segment independently instead of globally across all hops.
📍 Affects 2 files
  • packages/bruno-electron/src/ipc/network/axios-instance.js#L234-L273 (this comment)
  • packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js#L11-L40
🤖 Prompt for 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.

In `@packages/bruno-electron/src/ipc/network/axios-instance.js` around lines 234 -
273, Make redirect timelines hop-aware in backfillSentHeaders at
packages/bruno-electron/src/ipc/network/axios-instance.js:234-273 by resetting
the existing-header set and insertion boundary at the most recent request
marker, so only the current hop is scanned. Update sortRequestHeaderLogs at
packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js:11-40
to split slots at each request entry and sort/reassign request headers
independently within each hop segment.

Comment on lines +238 to +255
const parseSentHeaders = (req) => {
const raw = req?._header;
if (typeof raw === 'string') {
return raw
.split('\r\n')
.slice(1) // drop the request line (e.g. "GET /path HTTP/1.1")
.filter(Boolean)
.map((line) => {
const idx = line.indexOf(':');
return idx === -1 ? null : { name: line.slice(0, idx).trim(), value: line.slice(idx + 1).trim() };
})
.filter((h) => h && h.name);
}
const hdrs = typeof req?.getHeaders === 'function' ? req.getHeaders() : null;
return hdrs
? Object.entries(hdrs).map(([name, value]) => ({ name, value: Array.isArray(value) ? value.join(', ') : String(value) }))
: [];
};

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Is Node.js http ClientRequest._header a stable internal property across recent Node LTS versions?

💡 Result:

No, http.ClientRequest._header is not a stable or public API in Node.js [1][2][3]. It is an internal property that has never been documented as officially supported [3]. While it may still exist in some versions of Node.js for legacy compatibility or internal use [4][5], relying on it is strongly discouraged as it is subject to change or removal without notice [6]. It is important to distinguish _header from the deprecated _headers and _headerNames properties [6][3]. While _headers and _headerNames were formally deprecated (DEP0066) in Node.js v12.0.0 because they were internal, access to them triggers runtime warnings [3][7]. The _header property similarly falls under the category of non-public internal implementation details [3]. For working with HTTP headers in Node.js, you should always use the official public API methods [6][3]: - request.setHeader(name, value) [1] - request.getHeader(name) [1] - request.removeHeader(name) [1] - request.getHeaders() [6] - request.getHeaderNames() [3] If you need access to the final headers, use the provided public methods instead of attempting to access private properties prefixed with an underscore [6][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefon

echo "== locate axios-instance =="
fd -a 'axios-instance.js$' . || true

echo "== relevant file section =="
file="$(fd 'axios-instance.js$' packages/bruno-electron/src/ipc/network | head -n 1 || true)"
if [ -n "${file:-}" ]; then
  sed -n '210,270p' "$file" | cat -n
  echo "== package/version signals =="
  fd 'package.json$' . | sed -n '1,20p' | while read -r p; do
    echo "-- $p --"
    sed -n '1,80p' "$p" | rg -n '"node"|"electron"|"engines"|"prebuild"|"`@node`"' || true
  done
  echo "== _header/getHeaders usages =="
  rg -n "_header|getHeaders\\(" packages/bruno-electron/src/ipc/network/axios-instance.js packages || true
fi

Repository: usebruno/bruno

Length of output: 200


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate axios-instance =="
fd 'axios-instance.js$' . || true

file="$(fd 'axios-instance.js$' .packages/bruno-electron/src/ipc/network 2>/dev/null || fd 'axios-instance.js$' packages/bruno-electron/src/ipc/network | head -n 1 || true)"
echo "file=$file"

if [ -n "${file:-}" ]; then
  echo "== relevant file section =="
  sed -n '210,270p' "$file"

  echo "== package/version signals =="
  fd 'package.json$' . | sed -n '1,20p' | while read -r p; do
    echo "-- $p --"
    sed -n '1,120p' "$p" | rg -n '"node"|"electron"|"engines"|"prebuild"|"`@node`"' || true
  done

  echo "== _header/getHeaders usages =="
  rg -n "_header|getHeaders\\(" packages/bruno-electron/src/ipc/network/axios-instance.js packages || true
fi

Repository: usebruno/bruno

Length of output: 49314


Don’t depend on ClientRequest._header for request-header timeline backfill.

_header is a non-API Node http.ClientRequest implementation detail. If it disappears in the Node/Electron runtime, the parser falls back to getHeaders(), which uses lowercased header names and different casing/ordering. Use an API-supported approach for backfilling wire headers.

🤖 Prompt for 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.

In `@packages/bruno-electron/src/ipc/network/axios-instance.js` around lines 238 -
255, Update parseSentHeaders to stop reading the private ClientRequest._header
field. Use an API-supported source or mechanism that preserves the sent
wire-header names and ordering for timeline backfill, while retaining the
existing fallback behavior only if it provides equivalent wire-header data; do
not rely on getHeaders() as a casing/order substitute.

Comment on lines 140 to 153
setHeaders(headers) {
// Array form (e.g. [{ 'header-1': 'value-1' }, ...]) merges each entry onto the existing
// headers, preserving ones set elsewhere. Object form replaces the whole header set.
if (Array.isArray(headers)) {
if (!this.req.headers || typeof this.req.headers !== 'object' || Array.isArray(this.req.headers)) {
this.req.headers = {};
}
headers.forEach((entry) => {
if (entry && typeof entry === 'object') Object.assign(this.req.headers, entry);
});
return;
}
this.req.headers = headers;
}

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Prototype pollution risk via array-form setHeaders.

Object.assign(this.req.headers, entry) merges arbitrary keys from script-supplied objects; a __proto__/constructor/prototype key would pollute the prototype chain.

Based on the static analysis hint: "Merging request-derived input into a target object via Object.assign ... can pollute Object.prototype when the payload contains "proto"..." (prototype-pollution-recursive-merge-javascript).

🛡️ Proposed fix — filter dangerous keys
       headers.forEach((entry) => {
-        if (entry && typeof entry === 'object') Object.assign(this.req.headers, entry);
+        if (entry && typeof entry === 'object') {
+          Object.keys(entry).forEach((key) => {
+            if (key === '__proto__' || key === 'constructor' || key === 'prototype') return;
+            this.req.headers[key] = entry[key];
+          });
+        }
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setHeaders(headers) {
// Array form (e.g. [{ 'header-1': 'value-1' }, ...]) merges each entry onto the existing
// headers, preserving ones set elsewhere. Object form replaces the whole header set.
if (Array.isArray(headers)) {
if (!this.req.headers || typeof this.req.headers !== 'object' || Array.isArray(this.req.headers)) {
this.req.headers = {};
}
headers.forEach((entry) => {
if (entry && typeof entry === 'object') Object.assign(this.req.headers, entry);
});
return;
}
this.req.headers = headers;
}
setHeaders(headers) {
// Array form (e.g. [{ 'header-1': 'value-1' }, ...]) merges each entry onto the existing
// headers, preserving ones set elsewhere. Object form replaces the whole header set.
if (Array.isArray(headers)) {
if (!this.req.headers || typeof this.req.headers !== 'object' || Array.isArray(this.req.headers)) {
this.req.headers = {};
}
headers.forEach((entry) => {
if (entry && typeof entry === 'object') {
Object.keys(entry).forEach((key) => {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') return;
this.req.headers[key] = entry[key];
});
}
});
return;
}
this.req.headers = headers;
}
🧰 Tools
🪛 ast-grep (0.44.1)

[error] 147-147: Merging request-derived input into a target object via Object.assign / lodash merge / extend / deep-set can pollute Object.prototype when the payload contains "proto", "constructor", or "prototype" keys. Use a prototype-pollution-safe merge (e.g. lodash >=4.17.12 with key filtering), strip dangerous keys, or build the result on Object.create(null).
Context: Object.assign(this.req.headers, entry)
Note: [CWE-1321] Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').

(prototype-pollution-recursive-merge-javascript)

🤖 Prompt for 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.

In `@packages/bruno-js/src/bruno-request.js` around lines 140 - 153, Update
setHeaders in the array-form merge path to filter out prototype-pollution keys
(__proto__, constructor, and prototype) from each script-supplied entry before
merging into this.req.headers. Preserve merging of all other valid header keys
and the existing object-form replacement behavior.

Source: Linters/SAST tools

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.

1 participant