1/**
2 * Models HTTP headers for the unstable HTTP client and server modules.
3 *
4 * `Headers` values are immutable maps keyed by lowercase header name. This
5 * module converts common header inputs into that shape, provides helpers for
6 * reading and updating header values, and redacts configured sensitive headers
7 * when values are inspected.
8 *
9 * @stability unstable
10 * @since 4.0.0
11 */
12import * as Context from "../Context.ts"
13import * as Equal from "../Equal.ts"
14import * as Equ from "../Equivalence.ts"
15import { dual } from "../Function.ts"
16import * as Hash from "../Hash.ts"
17import * as Inspectable from "../Inspectable.ts"
18import * as Option from "../Option.ts"
19import * as Predicate from "../Predicate.ts"
20import * as Record from "../Record.ts"
21import * as Redactable from "../Redactable.ts"
22import * as Redacted from "../Redacted.ts"
23import type { Mutable } from "../Types.ts"
24
25/**
26 * Runtime type identifier for `Headers` values.
27 *
28 * @stability unstable
29 * @category type IDs
30 * @since 4.0.0
31 */
32export const TypeId: unique symbol = Symbol.for("~effect/http/Headers")
33
34/**
35 * Type of the unique symbol used to brand `Headers` values.
36 *
37 * @stability unstable
38 * @category type IDs
39 * @since 4.0.0
40 */
41export type TypeId = typeof TypeId
42
43/**
44 * Returns `true` if the provided value is a `Headers` value.
45 *
46 * @stability unstable
47 * @category guards
48 * @since 4.0.0
49 */
50export const isHeaders = (u: unknown): u is Headers => Predicate.hasProperty(u, TypeId)
51
52/**
53 * Represents an immutable HTTP header collection keyed by lowercase header name.
54 *
55 * **Details**
56 *
57 * `Headers` values also support redaction through the `Redactable` protocol.
58 *
59 * @stability unstable
60 * @category models
61 * @since 4.0.0
62 */
63export interface Headers extends Redactable.Redactable {
64 readonly [TypeId]: TypeId
65 readonly [key: string]: string
66}
67
68// the properties are folded into the initializer (rather than a separate
69// `Object.defineProperties(Proto, ...)` statement) so the whole definition
70// is pure-annotated by the build and tree-shakable.
71const Proto = Object.defineProperties(Object.create(null), {
72 [TypeId]: {
73 value: TypeId
74 },
75 [Redactable.symbolRedactable]: {
76 value(this: Headers, context: Context.Context<never>): Record<string, string | Redacted.Redacted<string>> {
77 return redact(this, Context.get(context, CurrentRedactedNames))
78 }
79 },
80 toJSON: {
81 value(this: Headers) {
82 return Redactable.redact(this)
83 }
84 },
85 [Equal.symbol]: {
86 value(this: Headers, that: Headers): boolean {
87 return Equivalence(this, that)
88 }
89 },
90 [Hash.symbol]: {
91 value(this: Headers): number {
92 return Hash.structure(this)
93 }
94 },
95 toString: {
96 value: Inspectable.BaseProto.toString
97 },
98 [Inspectable.NodeInspectSymbol]: {
99 value: Inspectable.BaseProto[Inspectable.NodeInspectSymbol]
100 }
101})
102
103const make = (input: Record.ReadonlyRecord<string, string>): Mutable<Headers> =>
104 Object.assign(Object.create(Proto), input) as Headers
105
106/**
107 * Provides an `Equivalence` instance that compares `Headers` by header names
108 * and string values.
109 *
110 * @stability unstable
111 * @category instances
112 * @since 4.0.0
113 */
114export const Equivalence: Equ.Equivalence<Headers> = Record.makeEquivalence(Equ.strictEqual<string>())
115
116/**
117 * Input accepted when constructing headers.
118 *
119 * **Details**
120 *
121 * Records may contain string values, string arrays, or `undefined`; arrays are joined with `", "`, and `undefined` values are omitted.
122 *
123 * @stability unstable
124 * @category models
125 * @since 4.0.0
126 */
127export type Input =
128 | Record.ReadonlyRecord<string, string | ReadonlyArray<string> | undefined>
129 | Iterable<readonly [string, string]>
130
131/**
132 * An empty `Headers` collection.
133 *
134 * @stability unstable
135 * @category constructors
136 * @since 4.0.0
137 */
138export const empty: Headers = Object.create(Proto)
139
140/**
141 * Creates `Headers` from a record or iterable of header entries.
142 *
143 * **Details**
144 *
145 * Header names are normalized to lowercase. Array values in record input are joined with `", "`, and `undefined` values are omitted.
146 *
147 * @stability unstable
148 * @category constructors
149 * @since 4.0.0
150 */
151export const fromInput: (input?: Input) => Headers = (input) => {
152 if (input === undefined) {
153 return empty
154 } else if (Symbol.iterator in input) {
155 const out: Record<string, string> = Object.create(Proto)
156 for (const [k, v] of input) {
157 out[k.toLowerCase()] = v
158 }
159 return out as Headers
160 }
161 const out: Record<string, string> = Object.create(Proto)
162 for (const [k, v] of Object.entries(input)) {
163 if (Array.isArray(v)) {
164 out[k.toLowerCase()] = v.join(", ")
165 } else if (v !== undefined) {
166 out[k.toLowerCase()] = v as string
167 }
168 }
169 return out as Headers
170}
171
172/**
173 * Treats an existing record as `Headers` unsafely.
174 *
175 * **Gotchas**
176 *
177 * This mutates the record's prototype and does not normalize header names; callers must provide the expected lowercase keys.
178 *
179 * @stability unstable
180 * @category constructors
181 * @since 4.0.0
182 */
183export const fromRecordUnsafe = (input: Record.ReadonlyRecord<string, string>): Headers =>
184 Object.setPrototypeOf(input, Proto) as Headers
185
186/**
187 * Returns `true` when a header with the given name is present.
188 *
189 * **Details**
190 *
191 * The lookup lowercases the provided header name.
192 *
193 * @stability unstable
194 * @category combinators
195 * @since 4.0.0
196 */
197export const has: {
198 (key: string): (self: Headers) => boolean
199 (self: Headers, key: string): boolean
200} = dual<
201 (key: string) => (self: Headers) => boolean,
202 (self: Headers, key: string) => boolean
203>(2, (self, key) => key.toLowerCase() in self)
204
205/**
206 * Gets a header value by name safely.
207 *
208 * **Details**
209 *
210 * The lookup lowercases the provided header name and returns `Option.none()` when absent.
211 *
212 * @stability unstable
213 * @category combinators
214 * @since 4.0.0
215 */
216export const get: {
217 (key: string): (self: Headers) => Option.Option<string>
218 (self: Headers, key: string): Option.Option<string>
219} = dual<
220 (key: string) => (self: Headers) => Option.Option<string>,
221 (self: Headers, key: string) => Option.Option<string>
222>(2, (self, key) => Option.fromUndefinedOr(self[key.toLowerCase()]))
223
224/**
225 * Returns a new `Headers` collection with the given header set.
226 *
227 * **Details**
228 *
229 * The header name is normalized to lowercase.
230 *
231 * @stability unstable
232 * @category combinators
233 * @since 4.0.0
234 */
235export const set: {
236 (key: string, value: string): (self: Headers) => Headers
237 (self: Headers, key: string, value: string): Headers
238} = dual<
239 (key: string, value: string) => (self: Headers) => Headers,
240 (self: Headers, key: string, value: string) => Headers
241>(3, (self, key, value) => {
242 const out = make(self)
243 out[key.toLowerCase()] = value
244 return out
245})
246
247/**
248 * Returns a new `Headers` collection with all provided headers set.
249 *
250 * **Details**
251 *
252 * Input headers are normalized with `fromInput` and override existing headers with the same lowercase name.
253 *
254 * @stability unstable
255 * @category combinators
256 * @since 4.0.0
257 */
258export const setAll: {
259 (headers: Input): (self: Headers) => Headers
260 (self: Headers, headers: Input): Headers
261} = dual<
262 (headers: Input) => (self: Headers) => Headers,
263 (self: Headers, headers: Input) => Headers
264>(2, (self, headers) =>
265 make({
266 ...self,
267 ...fromInput(headers)
268 }))
269
270/**
271 * Returns a new `Headers` collection containing headers from both collections.
272 *
273 * **Details**
274 *
275 * Headers from the second collection override headers from the first collection with the same name.
276 *
277 * @stability unstable
278 * @category combinators
279 * @since 4.0.0
280 */
281export const merge: {
282 (headers: Headers): (self: Headers) => Headers
283 (self: Headers, headers: Headers): Headers
284} = dual<
285 (headers: Headers) => (self: Headers) => Headers,
286 (self: Headers, headers: Headers) => Headers
287>(2, (self, headers) => {
288 const out = make(self)
289 Object.assign(out, headers)
290 return out
291})
292
293/**
294 * Returns a new `Headers` collection with the named header removed.
295 *
296 * **Details**
297 *
298 * The provided header name is normalized to lowercase before removal.
299 *
300 * @stability unstable
301 * @category combinators
302 * @since 4.0.0
303 */
304export const remove: {
305 (key: string): (self: Headers) => Headers
306 (self: Headers, key: string): Headers
307} = dual<
308 (key: string) => (self: Headers) => Headers,
309 (self: Headers, key: string) => Headers
310>(2, (self, key) => {
311 const out = make(self)
312 delete out[key.toLowerCase()]
313 return out
314})
315
316/**
317 * Returns a new `Headers` collection with each named header removed.
318 *
319 * **Details**
320 *
321 * Each provided header name is normalized to lowercase before removal.
322 *
323 * @stability unstable
324 * @category combinators
325 * @since 4.0.0
326 */
327export const removeMany: {
328 (keys: Iterable<string>): (self: Headers) => Headers
329 (self: Headers, keys: Iterable<string>): Headers
330} = dual<
331 (keys: Iterable<string>) => (self: Headers) => Headers,
332 (self: Headers, keys: Iterable<string>) => Headers
333>(2, (self, keys) => {
334 const out = make(self)
335 for (const key of keys) {
336 delete out[key.toLowerCase()]
337 }
338 return out
339})
340
341/**
342 * Returns a plain record with selected header values wrapped in `Redacted`.
343 *
344 * **Details**
345 *
346 * String keys are normalized to lowercase before matching; regular expressions are tested against the stored header names.
347 *
348 * @stability unstable
349 * @category combinators
350 * @since 4.0.0
351 */
352export const redact: {
353 (
354 key: string | RegExp | ReadonlyArray<string | RegExp>
355 ): (self: Headers) => Record<string, string | Redacted.Redacted>
356 (
357 self: Headers,
358 key: string | RegExp | ReadonlyArray<string | RegExp>
359 ): Record<string, string | Redacted.Redacted>
360} = dual(
361 2,
362 (
363 self: Headers,
364 key: string | RegExp | ReadonlyArray<string | RegExp>
365 ): Record<string, string | Redacted.Redacted> => {
366 const out: Record<string, string | Redacted.Redacted> = { ...self }
367 const modify = (key: string | RegExp) => {
368 if (typeof key === "string") {
369 const k = key.toLowerCase()
370 if (k in self) {
371 out[k] = Redacted.make(self[k])
372 }
373 } else {
374 for (const name in self) {
375 if (name.search(key) !== -1) {
376 out[name] = Redacted.make(self[name])
377 }
378 }
379 }
380 }
381 if (Array.isArray(key)) {
382 for (let i = 0; i < key.length; i++) {
383 modify(key[i])
384 }
385 } else {
386 modify(key as string | RegExp)
387 }
388 return out
389 }
390)
391
392/**
393 * Checks whether a header name matches one of the redaction patterns.
394 *
395 * **Details**
396 *
397 * String patterns are compared case-insensitively against the header name;
398 * regular expressions are tested against it. Use to avoid the record copy of
399 * `redact` when only a membership check is needed.
400 *
401 * @stability unstable
402 * @category combinators
403 * @since 4.0.0
404 */
405export const isRedactedName = (
406 name: string,
407 patterns: ReadonlyArray<string | RegExp>
408): boolean => {
409 for (let i = 0; i < patterns.length; i++) {
410 const pattern = patterns[i]
411 if (typeof pattern === "string") {
412 if (pattern.toLowerCase() === name.toLowerCase()) {
413 return true
414 }
415 } else if (name.search(pattern) !== -1) {
416 return true
417 }
418 }
419 return false
420}
421
422/**
423 * Context reference listing header names or patterns that should be redacted when `Headers` are inspected or rendered.
424 *
425 * **Details**
426 *
427 * Defaults include `authorization`, `cookie`, `set-cookie`, and `x-api-key`.
428 *
429 * @stability unstable
430 * @category services
431 * @since 4.0.0
432 */
433export const CurrentRedactedNames = Context.Reference<
434 ReadonlyArray<string | RegExp>
435>("effect/Headers/CurrentRedactedNames", {
436 defaultValue: () => [
437 "authorization",
438 "cookie",
439 "set-cookie",
440 "x-api-key"
441 ]
442})