Skip to content

fix(csod): send bootstrap session cookies; repair fetchResponse() - #2769

Open
bigguy6883 wants to merge 1 commit into
santifer:mainfrom
bigguy6883:fix/csod-session-cookies
Open

fix(csod): send bootstrap session cookies; repair fetchResponse()#2769
bigguy6883 wants to merge 1 commit into
santifer:mainfrom
bigguy6883:fix/csod-session-cookies

Conversation

@bigguy6883

@bigguy6883 bigguy6883 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Closes #2768

Two coupled bugs: CSOD tenants that enforce session cookies scan as 401 / zero jobs, and the helper that would fix it — fetchResponse() — has never worked.

1. csod.mjs — replay the bootstrap session cookies

CSOD's /services/x/career-site/v1/search needs the cookies the bootstrap home page sets alongside the anonymous JWT. The provider read that page with ctx.fetchText(), which discards response headers, so Set-Cookie was dropped and every search arrived unauthenticated.

The bootstrap now goes through ctx.fetchResponse so Set-Cookie is visible, and the cookies are replayed as a cookie header on each search POST.

  • New exported helper cookieHeaderFrom() — keeps the leading name=value, strips attributes (path/HttpOnly/Secure/SameSite/Expires) since those describe browser-jar storage rules and mean nothing on a request, last-wins per name.
  • Tenants that set no cookies get no cookie header, not an empty one. This matters: career-ohb works today without cookies and must keep working.
  • Falls back to ctx.fetchText when a ctx has no fetchResponse, so older embedders and existing test mocks are unaffected.
  • cfg.homeUrl and cfg.searchApi are built from the same parsed origin, so the replayed cookies cannot reach a third-party host.

The header comment claiming "no login, no session cookies needed" is corrected — that assumption is what the bug was made of.

2. _http.mjs — make fetchResponse() actually run

export async function fetchResponse(url, opts = {}) {
  return await fetchWithTimeout(url, opts);   // `consume` never passed
}

fetchWithTimeout ends in return await consume(res), so this threw TypeError: consume is not a function on every call. It went unnoticed because it has no callers — git grep matches only _http.mjs and _types.js. (Its comment cites a startup.ch provider as precedent; that provider isn't in this repo.)

Why not just (res) => res: returning the live Response would reintroduce the failure documented directly above it in the same file — a server that sends headers then stalls its body hangs the caller with the abort timer already cleared, which that comment records as having "froze full-directory sweeps silently". So the body is read inside the timer window and returned as an equivalent Response. Callers keep the standard { headers, status, text() } shape without inheriting the stall.

Null-body statuses (204/205/304) are guarded — the Response constructor throws on those with a body. Repeated Set-Cookie survives the reconstruction, which is the entire point of the helper and is now pinned by a test.

Since the function never successfully executed, there's no prior behaviour to preserve.

Verification

Live, against the public careers-kln.csod.com tenant (siteId 14):

before:  Total jobs found: 0   ✗ HTTP 401 CSOD Unauthorized Exception:Check your credentials.
after:   Total jobs found: 34

34 matches the totalCount returned by a hand-issued request with a cookie jar, so the board is fully enumerated, not partially.

Tests added:

  • tests/providers/csod.test.mjs — cookie replay with attributes stripped; header omitted when the tenant sets none; fetchText fallback when ctx has no fetchResponse.
  • tests/providers/_http.test.mjsfetchResponse() preserves repeated Set-Cookie, body stays readable, 204 doesn't throw. These fail on main with consume is not a function.

Suite status on this branch: provider suite 1,410 passed / 0 failed across 79 files; tests/providers/_http.test.mjs and tests/providers/csod.test.mjs green. (test-all.mjs OOMs on my ARM64 box, so files were run individually.)

Both tests were confirmed red before the fix.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed response handling for empty-body responses, including successful requests that return no content.
    • Preserved response headers, status information, and readable response bodies.
    • Improved authentication for CSOD searches by retaining session cookies from the initial connection.
    • Added compatibility for environments using the previous text-based response handling.
  • Tests
    • Added coverage for repeated cookies, empty responses, session-cookie reuse, and fallback behavior.

CSOD's search API requires the session cookies its bootstrap home page sets,
in addition to the anonymous JWT. csod.mjs read that page with ctx.fetchText(),
which discards response headers, so Set-Cookie was dropped and every search
call arrived unauthenticated. Tenants that enforce this returned
"HTTP 401 CSOD Unauthorized" and contributed zero jobs to a scan.

Fixing it required repairing providers/_http.mjs first: fetchResponse() called
the internal fetchWithTimeout() without its required `consume` argument, so it
threw "consume is not a function" on every invocation. It had no callers, so
nothing surfaced it. It now buffers the body inside the timer window and
returns an equivalent Response — returning the live Response would reintroduce
the stalled-body hang documented in that same file. Repeated Set-Cookie
survives the reconstruction; null-body statuses are guarded.

csod.mjs prefers ctx.fetchResponse and falls back to ctx.fetchText when a ctx
predates it, so existing callers are unaffected. Tenants that set no cookies
get no cookie header at all rather than an empty one.

Verified live against careers-kln.csod.com: 401 with zero jobs before, 34 jobs
after. Also corrects the csod header comment, which asserted that no session
cookies were needed.

Closes santifer#2768

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The HTTP helper now returns a readable reconstructed response with preserved metadata and headers. CSOD captures bootstrap session cookies and sends them with search requests when available, while older contexts retain the text-fetch fallback.

Changes

CSOD session authentication

Layer / File(s) Summary
Reconstruct readable HTTP responses
providers/_http.mjs, tests/providers/_http.test.mjs
fetchResponse consumes response bodies before the timeout ends and preserves response metadata, repeated cookies, readable bodies, and null-body statuses.
Capture and replay CSOD session cookies
providers/csod.mjs, tests/providers/csod.test.mjs
CSOD converts bootstrap Set-Cookie values into a Cookie header, sends it on searches when present, and retains fetchText compatibility when fetchResponse is unavailable.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: ilyakanevskiy

🚥 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 identifies both primary fixes: CSOD cookie replay and the repaired fetchResponse() helper.
Linked Issues check ✅ Passed The changes satisfy issue #2768 by replaying bootstrap cookies, repairing fetchResponse(), preserving compatibility, and adding regression tests.
Out of Scope Changes check ✅ Passed The implementation, documentation, and tests remain focused on the two bugs described in issue #2768.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 `@providers/csod.mjs`:
- Line 188: Update the bootstrap request in the CSOD provider around
ctx.fetchResponse to pass redirect: 'error' alongside the existing HTML Accept
header, ensuring redirects are rejected before following any destination.
- Line 213: Update resolveConfig() and the cookie-handling flow to require HTTPS
before retrieving or replaying session cookies. Reject configurations whose CSOD
URLs use http: and ensure the cookie header is never sent for non-HTTPS
requests, while preserving existing HTTPS behavior.

In `@tests/providers/_http.test.mjs`:
- Line 113: Update the cookie extraction near getSetCookie in the test to
support Node.js 18 by using a compatible Set-Cookie fallback when
Headers.getSetCookie() is unavailable, or raise the package’s minimum supported
Node.js version to 20. Ensure CSOD bootstrap cookies are preserved on all
supported runtimes.
🪄 Autofix

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 Plus

Run ID: 62525efa-a376-4d9b-87ec-c4796af34d9d

📥 Commits

Reviewing files that changed from the base of the PR and between 03fc92b and 2522c37.

📒 Files selected for processing (4)
  • providers/_http.mjs
  • providers/csod.mjs
  • tests/providers/_http.test.mjs
  • tests/providers/csod.test.mjs

Comment thread providers/csod.mjs
let html;
let cookie = '';
if (typeof ctx.fetchResponse === 'function') {
const res = await ctx.fetchResponse(cfg.homeUrl, { headers: { accept: 'text/html' } });

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

Reject bootstrap redirects.

Line 188 does not pass redirect: 'error'. fetchWithTimeout() then uses its follow default. A permitted CSOD host can redirect the bootstrap request to a private IP address. The origin validation only protects the initial URL.

Pass redirect: 'error' for the bootstrap request.

Proposed fix
-const res = await ctx.fetchResponse(cfg.homeUrl, { headers: { accept: 'text/html' } });
+const res = await ctx.fetchResponse(cfg.homeUrl, {
+  redirect: 'error',
+  headers: { accept: 'text/html' },
+});

As per path instructions: “Check for command injection, path traversal, and SSRF.”

📝 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
const res = await ctx.fetchResponse(cfg.homeUrl, { headers: { accept: 'text/html' } });
const res = await ctx.fetchResponse(cfg.homeUrl, {
redirect: 'error',
headers: { accept: 'text/html' },
});
🤖 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 `@providers/csod.mjs` at line 188, Update the bootstrap request in the CSOD
provider around ctx.fetchResponse to pass redirect: 'error' alongside the
existing HTML Accept header, ensuring redirects are rejected before following
any destination.

Source: Path instructions

Comment thread providers/csod.mjs
'content-type': 'application/json',
accept: 'application/json',
authorization: `Bearer ${token}`,
...(cookie ? { cookie } : {}),

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

Do not replay session cookies over HTTP.

resolveConfig() accepts http: at Line 48 and preserves that protocol in both CSOD URLs. This new header then sends bootstrap session cookies in plaintext when an entry uses an HTTP URL. Require HTTPS before the provider retrieves or replays cookies.

Proposed fix
-if (u.protocol !== 'https:' && u.protocol !== 'http:') return null;
+if (u.protocol !== 'https:') return null;

As per path instructions: “Check for command injection, path traversal, and SSRF.”

🤖 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 `@providers/csod.mjs` at line 213, Update resolveConfig() and the
cookie-handling flow to require HTTPS before retrieving or replaying session
cookies. Reject configurations whose CSOD URLs use http: and ensure the cookie
header is never sent for non-HTTPS requests, while preserving existing HTTPS
behavior.

Source: Path instructions

headers.append('set-cookie', 'tenant=kln; Secure');
stub('{"token":"tok"}', { status: 200, headers });
const res = await fetchResponse('https://example.com/home');
const cookies = res.headers.getSetCookie();

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -H -t f -a '^(package\.json|\.nvmrc|\.node-version|\.tool-versions)$' . \
  -x sh -c 'echo "=== $1 ==="; sed -n "1,160p" "$1"' sh {}

rg -n -C 2 'node-version|setup-node|NODE_VERSION|"node"\s*:' \
  package.json .github 2>/dev/null || true

Repository: santifer/career-ops

Length of output: 9680


🌐 Web query:

Node.js Headers.getSetCookie() added version official documentation

💡 Result:

The Headers.prototype.getSetCookie() method was added to the undici library—which powers the Fetch API implementation in Node.js—in version 5.19.0 [1]. Consequently, native support for getSetCookie() became available in Node.js core starting with the release of Node.js v20.0.0 [2]. This method is used to retrieve an array of all Set-Cookie header values associated with a response [3]. It is necessary because the standard Headers.get() method attempts to join multiple Set-Cookie values into a single string (often using a comma), which can incorrectly parse cookie values that contain commas (such as those in Expires attributes) [2][4]. Using getSetCookie() ensures each cookie string is correctly isolated [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== test file context ==='
sed -n '90,125p' tests/providers/_http.test.mjs

printf '%s\n' '=== CSOD cookie feature check ==='
rg -n -C 5 'getSetCookie|set-cookie|bootstrap|cookie' providers/csod.mjs tests/providers/_http.test.mjs

printf '%s\n' '=== repository test/runtime configuration ==='
rg -n -C 3 'tests/providers|node --test|engines|node-version|setup-node' package.json package-lock.json .github 2>/dev/null || true

Repository: santifer/career-ops

Length of output: 17753


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json
import re
from pathlib import Path

root = json.loads(Path("package.json").read_text())
engine = root.get("engines", {}).get("node")
source = Path("providers/csod.mjs").read_text()
test = Path("tests/providers/_http.test.mjs").read_text()

ci_versions = {}
for path in Path(".github/workflows").glob("*.yml"):
    text = path.read_text()
    versions = re.findall(r"node-version:\s*['\"]?([0-9]+)", text)
    if versions:
        ci_versions[str(path)] = versions

print(f"root engines.node = {engine!r}")
print(f"root engine includes Node 18 = {engine == '>=18'}")
print(f"test calls getSetCookie directly = {bool(re.search(r'headers\\.getSetCookie\\(\\)', test))}")
print(f"provider feature-checks getSetCookie = {bool(re.search(r'typeof res\\?\\.headers\\?\\.getSetCookie === .function.', source))}")
print(f"provider substitutes no cookies when unsupported = {\" : []\" in source or \" : []\" in source}")
print(f"CI node versions = {ci_versions}")
PY

Repository: santifer/career-ops

Length of output: 460


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json
import re
from pathlib import Path

root = json.loads(Path("package.json").read_text())
engine = root.get("engines", {}).get("node")
source = Path("providers/csod.mjs").read_text()
test = Path("tests/providers/_http.test.mjs").read_text()

direct_call = bool(re.search(r"headers\.getSetCookie\(\)", test))
feature_check = bool(re.search(r"typeof res\?\.headers\?\.getSetCookie === ['\"]function['\"]", source))
empty_fallback = bool(re.search(r"getSetCookie\(\)\s*:\s*\[\]", source))

ci_versions = {}
for path in Path(".github/workflows").glob("*.yml"):
    versions = re.findall(r"node-version:\s*['\"]?([0-9]+)", path.read_text())
    if versions:
        ci_versions[str(path)] = versions

print(f"root engines.node = {engine!r}")
print(f"root engine includes Node 18 = {engine == '>=18'}")
print(f"test calls getSetCookie directly = {direct_call}")
print(f"provider feature-checks getSetCookie = {feature_check}")
print(f"provider uses an empty-array fallback = {empty_fallback}")
print(f"CI node versions = {ci_versions}")
PY

Repository: santifer/career-ops

Length of output: 558


Support Node.js 20 or add a Set-Cookie fallback.

The package supports Node.js >=18, but the test calls Headers.getSetCookie() directly and CI only tests Node.js 24. On Node.js 18, CSOD bootstrap cookies are omitted, which can cause HTTP 401 CSOD Unauthorized.

🤖 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 `@tests/providers/_http.test.mjs` at line 113, Update the cookie extraction
near getSetCookie in the test to support Node.js 18 by using a compatible
Set-Cookie fallback when Headers.getSetCookie() is unavailable, or raise the
package’s minimum supported Node.js version to 20. Ensure CSOD bootstrap cookies
are preserved on all supported runtimes.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

csod: search API 401s on tenants requiring the bootstrap session cookie; fetchResponse() is broken

1 participant