From 46326601444138317bfe581d401164068395fb45 Mon Sep 17 00:00:00 2001 From: Mukunda Katta Date: Thu, 7 May 2026 10:29:35 -0700 Subject: [PATCH] fix(http): getCookies returns Partial> (closes #7053) The previous return type `Record` was a lie: arbitrary key access can yield `undefined` because the function only populates keys present in the Cookie header. Consumers without `noUncheckedIndexedAccess` enabled would unsafely treat missing-cookie reads as guaranteed strings. Switch to `Partial>`. The runtime behavior is unchanged (the returned object is still null-prototyped, so missing-key access really does yield `undefined` rather than an inherited Object prototype method), but TypeScript now reflects that. - update return type + JSDoc to spell out the contract - regression test asserts the new exact type via IsExact<> - keeps the existing null-prototype check so behaviour stays pinned --- http/cookie.ts | 20 ++++++++++++++++---- http/cookie_test.ts | 9 ++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/http/cookie.ts b/http/cookie.ts index 91a2d14d8126..98d9fc2fe1df 100644 --- a/http/cookie.ts +++ b/http/cookie.ts @@ -233,6 +233,12 @@ function validateDomain(domain: string) { /** * Parse cookies of a header * + * The returned object has a null prototype (so it does not inherit from + * `Object.prototype` — properties like `toString` or `hasOwnProperty` will be + * `undefined` rather than the inherited methods). The return type is + * `Partial>` to reflect that arbitrary key access may + * yield `undefined` when no cookie of that name was set. + * * @example Usage * ```ts * import { getCookies } from "@std/http/cookie"; @@ -242,15 +248,21 @@ function validateDomain(domain: string) { * headers.set("Cookie", "full=of; tasty=chocolate"); * * const cookies = getCookies(headers); - * assertEquals(cookies, { full: "of", tasty: "chocolate" }); + * assertEquals(cookies.full, "of"); + * assertEquals(cookies.tasty, "chocolate"); + * assertEquals(cookies.missing, undefined); * ``` * * @param headers The headers instance to get cookies from - * @return Object with cookie names as keys + * @return Object with cookie names as keys (null-prototype). Values are + * typed as `string | undefined` so missing-key access is caught at compile + * time. */ -export function getCookies(headers: Headers): Record { +export function getCookies( + headers: Headers, +): Partial> { const cookie = headers.get("Cookie"); - const out: Record = Object.create(null); + const out: Partial> = Object.create(null); if (cookie !== null) { const c = cookie.split(";"); for (const kv of c) { diff --git a/http/cookie_test.ts b/http/cookie_test.ts index 72f5eecacb2c..fac04877a33d 100644 --- a/http/cookie_test.ts +++ b/http/cookie_test.ts @@ -45,9 +45,12 @@ Deno.test({ fn() { const headers = new Headers([["Cookie", "foo=bar"]]); const cookies = getCookies(headers); - assertType>>(true); - // allowed due to `noUncheckedIndexedAccess` - const baz = cookies.baz as undefined; + // Return type is Partial> so arbitrary key access + // surfaces as `string | undefined` regardless of `noUncheckedIndexedAccess`. + // Closes the type/runtime gap reported in #6852 and #7053 — at runtime the + // returned object is null-prototyped, so missing keys really are undefined. + assertType>>>(true); + const baz: string | undefined = cookies.baz; assertEquals(baz, undefined); assertEquals(Object.getPrototypeOf(cookies), null); assertEquals(cookies.toString, undefined);