Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ name: πŸ—οΈ Build Native Modules

on:
pull_request:
paths:
- '.github/workflows/build.yml'
- 'package.json'
- 'package-lock.json'
- 'rust/**'
- 'scripts/**'
release:
types: [published]

Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,23 @@ name: πŸ§ͺ Test

on:
push:
paths:
- '.github/workflows/test.yml'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'src/**'
- 'rust/**'
- 'scripts/**'
pull_request:
paths:
- '.github/workflows/test.yml'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'src/**'
- 'rust/**'
- 'scripts/**'

jobs:
test:
Expand Down
8 changes: 4 additions & 4 deletions .skills/release-notes/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,10 @@ Generate accurate release notes from the git history since the previous release.
- `Dependencies`
- `Breaking Changes`

7. Write release notes in this shape unless the user requests another format:
7. Write release notes in this shape:

```markdown
## <version or "Unreleased">

Compared with `<base tag>`.
## Release Notes

### Added
- ...
Expand All @@ -98,5 +96,7 @@ Generate accurate release notes from the git history since the previous release.
- Mention internal-only test, lint, formatting, and CI work only under `Tests` or `Build & CI`.
- Keep bullets concrete and scoped: name the API, feature, platform, or workflow affected.
- Include the compared range and base tag in the final answer.
- Always return the final release notes inside one outer fenced Markdown code block using four backticks and `md` as the language: ````md ... ````. This is mandatory even when the user does not explicitly ask for a code block.
- If the release notes contain code examples, keep normal triple-backtick fences inside the outer four-backtick block so the response does not terminate early.
- If the range is empty, say no changes were found after the selected release tag.
- If tags are missing or ambiguous, stop and ask for the intended base release instead of guessing.
13 changes: 9 additions & 4 deletions rust/src/napi/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,12 +705,17 @@ pub(crate) fn response_to_js_object<'a, C: Context<'a>>(
let url = cx.string(&response.url);
obj.set(cx, "url", url)?;

let headers_obj = cx.empty_object();
for (key, value) in response.headers {
let headers_array = JsArray::new(cx, response.headers.len());
for (index, (key, value)) in response.headers.into_iter().enumerate() {
let tuple = JsArray::new(cx, 2);
let key_str = cx.string(&key);
let value_str = cx.string(&value);
headers_obj.set(cx, key.as_str(), value_str)?;

tuple.set(cx, 0, key_str)?;
tuple.set(cx, 1, value_str)?;
headers_array.set(cx, index as u32, tuple)?;
}
obj.set(cx, "headers", headers_obj)?;
obj.set(cx, "headers", headers_array)?;

let cookies_obj = cx.empty_object();
for (key, value) in response.cookies {
Expand Down
7 changes: 3 additions & 4 deletions rust/src/transport/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use crate::transport::headers::build_orig_header_map;
use crate::transport::tls::configure_client_builder;
use crate::transport::types::{RequestOptions, Response, ResponseTlsInfo};
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::time::Duration;
use wreq::{redirect, tls::TlsInfo, Method};

Expand Down Expand Up @@ -165,14 +164,14 @@ pub async fn make_request(options: RequestOptions) -> Result<Response> {
let status = response.status().as_u16();
let final_url = response.uri().to_string();

let mut response_headers = HashMap::new();
let mut response_headers = Vec::new();
for (key, value) in response.headers() {
if let Ok(value_str) = value.to_str() {
response_headers.insert(key.to_string(), value_str.to_string());
response_headers.push((key.to_string(), value_str.to_string()));
}
}

let mut cookies = HashMap::new();
let mut cookies = std::collections::HashMap::new();
let mut set_cookies = Vec::new();
for cookie_header in response.headers().get_all("set-cookie") {
if let Ok(cookie_str) = cookie_header.to_str() {
Expand Down
2 changes: 1 addition & 1 deletion rust/src/transport/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ pub struct RequestOptions {
#[derive(Debug, Clone)]
pub struct Response {
pub status: u16,
pub headers: HashMap<String, String>,
pub headers: Vec<(String, String)>,
pub body_handle: u64,
pub cookies: HashMap<String, String>,
pub set_cookies: Vec<String>,
Expand Down
5 changes: 5 additions & 0 deletions src/headers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ export class Headers implements Iterable<HeaderTuple> {
return entry ? entry.values.join(', ') : null;
}

/** Returns all `Set-Cookie` header values without comma-joining them. */
getSetCookie(): string[] {
return [...(this.store.get('set-cookie')?.values ?? [])];
}

/** Returns `true` when the header collection contains `name`. */
has(name: string): boolean {
const normalized = this.normalizeName(name);
Expand Down
2 changes: 1 addition & 1 deletion src/http/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export class Response {
const cloned = new Response(null, {
status: this.status,
statusText: this.statusText,
headers: this.headers.toObject(),
headers: this.headers.toTuples(),
url: this.url,
});

Expand Down
14 changes: 14 additions & 0 deletions src/test/helpers/local-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,20 @@ export function setupLocalTestServer() {
return;
}

if (url.pathname === '/headers/duplicates') {
sendJson(
response,
200,
{ ok: true },
{
'x-dupe': ['one', 'two'],
'set-cookie': ['session=abc123; Path=/', 'csrf=token123; Path=/'],
}
);

return;
}

if (url.pathname === '/cookies/echo') {
sendJson(response, 200, { cookie: readCookieHeader(request) });

Expand Down
22 changes: 22 additions & 0 deletions src/test/transport-features.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,28 @@ describe('transport features', () => {
assert.strictEqual(body.headers.host, `example.test:${target.port}`);
});

test('should preserve duplicate response headers like fetch', async () => {
const response = await fetch(`${getBaseUrl()}/headers/duplicates`);

assert.strictEqual(response.headers.get('x-dupe'), 'one, two');
assert.deepStrictEqual(response.headers.getSetCookie(), [
'session=abc123; Path=/',
'csrf=token123; Path=/',
]);
assert.strictEqual(
response.headers.get('set-cookie'),
'session=abc123; Path=/, csrf=token123; Path=/'
);

const cloned = response.clone();

assert.strictEqual(cloned.headers.get('x-dupe'), 'one, two');
assert.deepStrictEqual(cloned.headers.getSetCookie(), [
'session=abc123; Path=/',
'csrf=token123; Path=/',
]);
});

test('should reject non-HTTPS DoH endpoints', async () => {
await assert.rejects(
() =>
Expand Down
4 changes: 2 additions & 2 deletions src/types/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ export interface NativeResponse {
status: number;
/** HTTP reason phrase when provided by the transport. */
statusText?: string;
/** Response headers normalized to a plain object. */
headers: Record<string, string>;
/** Response headers as ordered `[name, value]` tuples. */
headers: HeaderTuple[];
/** Eagerly loaded UTF-8 response body, when available. */
body?: string;
/** Native body handle used for streaming large responses. */
Expand Down
Loading