Skip to content

Commit dd16061

Browse files
kamilioclaude
andcommitted
feat(har): parse cookies and redirect URL from headers
Extract additional HAR fields from existing log data: - Parse request cookies from Cookie header - Parse response cookies from Set-Cookie header (with path, domain, expires, httpOnly, secure attributes) - Extract redirectURL from Location header Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d2683c8 commit dd16061

2 files changed

Lines changed: 155 additions & 3 deletions

File tree

src/services/har-service.js

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,15 @@ function buildHarRequest(req) {
8282
const headers = objectToNameValuePairs(sanitizedHeaders);
8383
const queryString = extractQueryString(sanitizedUrl);
8484
const postData = buildPostData(sanitizedBody, sanitizedHeaders);
85+
const cookies = parseCookiesFromHeaders(sanitizedHeaders);
8586

8687
return {
8788
method: req.method || 'GET',
8889
url: sanitizedUrl,
8990
httpVersion: 'HTTP/1.1',
9091
headers,
9192
queryString,
92-
cookies: [],
93+
cookies,
9394
headersSize: -1,
9495
bodySize: postData ? postData.text.length : -1,
9596
...(postData && { postData }),
@@ -120,15 +121,17 @@ function buildHarResponse(res) {
120121

121122
const headers = objectToNameValuePairs(sanitizedHeaders);
122123
const content = buildContent(sanitizedBody, sanitizedHeaders);
124+
const cookies = parseSetCookiesFromHeaders(sanitizedHeaders);
125+
const redirectURL = extractRedirectURL(sanitizedHeaders);
123126

124127
return {
125128
status: res.status || 0,
126129
statusText: getStatusText(res.status),
127130
httpVersion: 'HTTP/1.1',
128131
headers,
129-
cookies: [],
132+
cookies,
130133
content,
131-
redirectURL: '',
134+
redirectURL,
132135
headersSize: -1,
133136
bodySize: content.size,
134137
};
@@ -223,3 +226,84 @@ function getStatusText(status) {
223226
};
224227
return statusTexts[status] || '';
225228
}
229+
230+
function parseCookiesFromHeaders(headers) {
231+
if (!headers || typeof headers !== 'object') {
232+
return [];
233+
}
234+
for (const [key, value] of Object.entries(headers)) {
235+
if (key.toLowerCase() === 'cookie') {
236+
return parseCookieHeader(String(value));
237+
}
238+
}
239+
return [];
240+
}
241+
242+
function parseCookieHeader(cookieStr) {
243+
if (!cookieStr) return [];
244+
return cookieStr.split(';').map((pair) => {
245+
const [name, ...rest] = pair.trim().split('=');
246+
return {
247+
name: name || '',
248+
value: rest.join('=') || '',
249+
};
250+
}).filter((c) => c.name);
251+
}
252+
253+
function parseSetCookiesFromHeaders(headers) {
254+
if (!headers || typeof headers !== 'object') {
255+
return [];
256+
}
257+
const cookies = [];
258+
for (const [key, value] of Object.entries(headers)) {
259+
if (key.toLowerCase() === 'set-cookie') {
260+
const values = Array.isArray(value) ? value : [value];
261+
for (const v of values) {
262+
const cookie = parseSetCookieValue(String(v));
263+
if (cookie) cookies.push(cookie);
264+
}
265+
}
266+
}
267+
return cookies;
268+
}
269+
270+
function parseSetCookieValue(setCookieStr) {
271+
if (!setCookieStr) return null;
272+
const parts = setCookieStr.split(';').map((p) => p.trim());
273+
if (parts.length === 0) return null;
274+
275+
const [nameValue, ...attributes] = parts;
276+
const [name, ...rest] = nameValue.split('=');
277+
if (!name) return null;
278+
279+
const cookie = {
280+
name,
281+
value: rest.join('=') || '',
282+
};
283+
284+
for (const attr of attributes) {
285+
const [attrName, ...attrRest] = attr.split('=');
286+
const attrLower = (attrName || '').toLowerCase();
287+
const attrValue = attrRest.join('=');
288+
289+
if (attrLower === 'path') cookie.path = attrValue;
290+
else if (attrLower === 'domain') cookie.domain = attrValue;
291+
else if (attrLower === 'expires') cookie.expires = attrValue;
292+
else if (attrLower === 'httponly') cookie.httpOnly = true;
293+
else if (attrLower === 'secure') cookie.secure = true;
294+
}
295+
296+
return cookie;
297+
}
298+
299+
function extractRedirectURL(headers) {
300+
if (!headers || typeof headers !== 'object') {
301+
return '';
302+
}
303+
for (const [key, value] of Object.entries(headers)) {
304+
if (key.toLowerCase() === 'location') {
305+
return String(value);
306+
}
307+
}
308+
return '';
309+
}

tests/har-service.test.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,74 @@ describe('logToHar', () => {
154154

155155
assert.strictEqual(postData.mimeType, 'application/json');
156156
});
157+
158+
it('should parse cookies from Cookie header', () => {
159+
const log = {
160+
timestamp: '2026-01-24T19:35:45.748Z',
161+
request: {
162+
method: 'GET',
163+
url: 'https://api.example.com/v1/chat',
164+
headers: {
165+
'cookie': 'session=abc123; user=john; token=xyz',
166+
},
167+
},
168+
response: { status: 200, headers: {}, body: {} },
169+
};
170+
171+
const har = logToHar(log);
172+
const cookies = har.log.entries[0].request.cookies;
173+
174+
assert.strictEqual(cookies.length, 3);
175+
assert.strictEqual(cookies[0].name, 'session');
176+
assert.strictEqual(cookies[0].value, 'abc123');
177+
assert.strictEqual(cookies[1].name, 'user');
178+
assert.strictEqual(cookies[1].value, 'john');
179+
});
180+
181+
it('should parse Set-Cookie headers in response', () => {
182+
const log = {
183+
timestamp: '2026-01-24T19:35:45.748Z',
184+
request: { method: 'GET', url: 'https://api.example.com/v1/chat', headers: {} },
185+
response: {
186+
status: 200,
187+
headers: {
188+
'set-cookie': 'session=abc123; Path=/; HttpOnly; Secure',
189+
},
190+
body: {},
191+
},
192+
};
193+
194+
const har = logToHar(log);
195+
const cookies = har.log.entries[0].response.cookies;
196+
197+
assert.strictEqual(cookies.length, 1);
198+
assert.strictEqual(cookies[0].name, 'session');
199+
assert.strictEqual(cookies[0].value, 'abc123');
200+
assert.strictEqual(cookies[0].path, '/');
201+
assert.strictEqual(cookies[0].httpOnly, true);
202+
assert.strictEqual(cookies[0].secure, true);
203+
});
204+
205+
it('should extract redirectURL from Location header', () => {
206+
const log = {
207+
timestamp: '2026-01-24T19:35:45.748Z',
208+
request: { method: 'GET', url: 'https://api.example.com/old', headers: {} },
209+
response: {
210+
status: 302,
211+
headers: {
212+
'location': 'https://api.example.com/new',
213+
},
214+
body: {},
215+
},
216+
};
217+
218+
const har = logToHar(log);
219+
const entry = har.log.entries[0];
220+
221+
assert.strictEqual(entry.response.redirectURL, 'https://api.example.com/new');
222+
assert.strictEqual(entry.response.status, 302);
223+
assert.strictEqual(entry.response.statusText, 'Found');
224+
});
157225
});
158226

159227
describe('logsToHar', () => {

0 commit comments

Comments
 (0)