Skip to content

Commit

Permalink
capture redactable context at creation
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart committed Dec 19, 2024
1 parent cd543e4 commit 552d625
Show file tree
Hide file tree
Showing 15 changed files with 168 additions and 134 deletions.
64 changes: 51 additions & 13 deletions packages/effect/src/Inspectable.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* @since 2.0.0
*/

import * as Context from "./Context.js"
import type { RuntimeFiber } from "./Fiber.js"
import type * as FiberRefs from "./FiberRefs.js"
import { globalValue } from "./GlobalValue.js"
import { hasProperty, isFunction } from "./Predicate.js"

/**
Expand Down Expand Up @@ -126,15 +126,15 @@ export const stringifyCircular = (obj: unknown, whitespace?: number | string | u
* @since 3.10.0
* @category redactable
*/
export interface Redactable {
readonly [symbolRedactable]: (fiberRefs: FiberRefs.FiberRefs) => unknown
}
export const symbolRedactable: unique symbol = Symbol.for("effect/Inspectable/Redactable")

/**
* @since 3.10.0
* @category redactable
*/
export const symbolRedactable: unique symbol = Symbol.for("effect/Inspectable/Redactable")
export interface Redactable {
[symbolRedactable](): unknown
}

/**
* @since 3.10.0
Expand All @@ -143,18 +143,56 @@ export const symbolRedactable: unique symbol = Symbol.for("effect/Inspectable/Re
export const isRedactable = (u: unknown): u is Redactable =>
typeof u === "object" && u !== null && symbolRedactable in u

const currentFiberURI = "effect/FiberCurrent"

/**
* @since 3.10.0
* @category redactable
*/
export const redact = (u: unknown): unknown => {
if (isRedactable(u)) {
const fiber = (globalThis as any)[currentFiberURI] as RuntimeFiber<any, any> | undefined
if (fiber !== undefined) {
return u[symbolRedactable](fiber.getFiberRefs())
return isRedactable(u) ? u[symbolRedactable]() : u
}

const redactableContext = globalValue("effect/Inspectable/redactableContext", () => new WeakMap<any, any>())

/**
* @since 3.12.0
* @category redactable
*/
export const makeRedactableContext = <A>(make: (context: Context.Context<never>) => A): {
readonly register: (self: unknown, context?: Context.Context<never>, input?: unknown) => void
readonly get: (u: unknown) => A
} => ({
register(self, context, input) {
if (input && redactableContext.has(input)) {
redactableContext.set(self, redactableContext.get(input))
return
}
redactableContext.set(self, make(context ?? currentContext() ?? Context.empty()))
},
get(u) {
return redactableContext.has(u) ? redactableContext.get(u) : make(currentContext() ?? Context.empty())
}
return u
})

/**
* @since 3.12.0
* @category redactable
*/
export const RedactableClass = <Self>() =>
<A>(options: {
readonly context: (fiberRefs: Context.Context<never>) => A
readonly redact: (self: Self, context: A) => unknown
}): new(context?: Context.Context<never>, input?: unknown) => Redactable => {
const redactable = makeRedactableContext(options.context)
function Redactable(this: any, context?: Context.Context<never>, input?: unknown) {
redactable.register(this, context, input)
}
Redactable.prototype[symbolRedactable] = function() {
return options.redact(this, redactable.get(this))
}
return Redactable as any
}

const currentContext = (): Context.Context<never> | undefined => {
const fiber = (globalThis as any)["effect/FiberCurrent"] as RuntimeFiber<any> | undefined
return fiber ? fiber.currentContext : undefined
}
4 changes: 2 additions & 2 deletions packages/platform-browser/src/internal/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,12 @@ export abstract class IncomingMessageImpl<E> extends Inspectable.Class
return this._headers
}
if (this._rawHeaderString === "") {
return this._headers = Headers.empty
return this._headers = Headers.empty()
}
const parser = HeaderParser.make()
const result = parser(encoder.encode(this._rawHeaderString + "\r\n"), 0)
this._rawHeaders = result._tag === "Headers" ? result.headers : undefined
const parsed = result._tag === "Headers" ? Headers.fromInput(result.headers) : Headers.empty
const parsed = result._tag === "Headers" ? Headers.fromInput(result.headers) : Headers.empty()
return this._headers = parsed
}

Expand Down
6 changes: 2 additions & 4 deletions packages/platform-bun/src/internal/httpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ function wsDefaultRun(this: WebSocketContext, _: Uint8Array | string) {
class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpServerRequest {
readonly [ServerRequest.TypeId]: ServerRequest.TypeId
readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId
readonly headers: Headers.Headers
constructor(
readonly source: Request,
public resolve: (response: Response) => void,
Expand All @@ -219,6 +220,7 @@ class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpS
super()
this[ServerRequest.TypeId] = ServerRequest.TypeId
this[IncomingMessage.TypeId] = IncomingMessage.TypeId
this.headers = headersOverride ?? Headers.fromInput(source.headers)
}
toJSON(): unknown {
return IncomingMessage.inspect(this, {
Expand Down Expand Up @@ -254,10 +256,6 @@ class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpS
? Option.some(this.remoteAddressOverride)
: Option.fromNullable(this.bunServer.requestIP(this.source)?.address)
}
get headers(): Headers.Headers {
this.headersOverride ??= Headers.fromInput(this.source.headers)
return this.headersOverride
}

private cachedCookies: ReadonlyRecord<string, string> | undefined
get cookies() {
Expand Down
6 changes: 2 additions & 4 deletions packages/platform-node/src/internal/httpClientUndici.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ function noopErrorHandler(_: any) {}
class ClientResponseImpl extends Inspectable.Class implements ClientResponse.HttpClientResponse {
readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId
readonly [ClientResponse.TypeId]: ClientResponse.TypeId
readonly headers: Headers.Headers

constructor(
readonly request: ClientRequest.HttpClientRequest,
Expand All @@ -105,6 +106,7 @@ class ClientResponseImpl extends Inspectable.Class implements ClientResponse.Htt
super()
this[IncomingMessage.TypeId] = IncomingMessage.TypeId
this[ClientResponse.TypeId] = ClientResponse.TypeId
this.headers = Headers.fromInput(this.source.headers)
source.body.on("error", noopErrorHandler)
}

Expand All @@ -128,10 +130,6 @@ class ClientResponseImpl extends Inspectable.Class implements ClientResponse.Htt
return this.cachedCookies = Cookies.empty
}

get headers(): Headers.Headers {
return Headers.fromInput(this.source.headers)
}

get remoteAddress(): Option.Option<string> {
return Option.none()
}
Expand Down
9 changes: 4 additions & 5 deletions packages/platform-node/src/internal/httpIncomingMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ export abstract class HttpIncomingMessageImpl<E> extends Inspectable.Class
implements IncomingMessage.HttpIncomingMessage<E>
{
readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId
readonly headers: Headers.Headers

constructor(
readonly source: Http.IncomingMessage,
readonly onError: (error: unknown) => E,
readonly remoteAddressOverride?: string
readonly remoteAddressOverride?: string,
readonly headersOverride?: Headers.Headers
) {
super()
this[IncomingMessage.TypeId] = IncomingMessage.TypeId
}

get headers() {
return Headers.fromInput(this.source.headers as any)
this.headers = headersOverride ?? Headers.fromInput(this.source.headers as any)
}

get remoteAddress() {
Expand Down
24 changes: 12 additions & 12 deletions packages/platform-node/src/internal/httpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,15 +213,20 @@ class ServerRequestImpl extends HttpIncomingMessageImpl<Error.RequestError> impl
readonly response: Http.ServerResponse | LazyArg<Http.ServerResponse>,
private upgradeEffect?: Effect.Effect<Socket.Socket, Error.RequestError>,
readonly url = source.url!,
private headersOverride?: Headers.Headers,
headersOverride?: Headers.Headers,
remoteAddressOverride?: string
) {
super(source, (cause) =>
new Error.RequestError({
request: this,
reason: "Decode",
cause
}), remoteAddressOverride)
super(
source,
(cause) =>
new Error.RequestError({
request: this,
reason: "Decode",
cause
}),
remoteAddressOverride,
headersOverride
)
this[ServerRequest.TypeId] = ServerRequest.TypeId
}

Expand Down Expand Up @@ -262,11 +267,6 @@ class ServerRequestImpl extends HttpIncomingMessageImpl<Error.RequestError> impl
return this.source.method!.toUpperCase() as HttpMethod
}

get headers(): Headers.Headers {
this.headersOverride ??= this.source.headers as Headers.Headers
return this.headersOverride
}

private multipartEffect:
| Effect.Effect<
Multipart.Persisted,
Expand Down
77 changes: 48 additions & 29 deletions packages/platform/src/Headers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/**
* @since 1.0.0
*/
import { FiberRefs } from "effect"
import * as FiberRef from "effect/FiberRef"
import * as Context from "effect/Context"
import { dual, identity } from "effect/Function"
import { globalValue } from "effect/GlobalValue"
import * as Inspectable from "effect/Inspectable"
import type * as Option from "effect/Option"
import * as Predicate from "effect/Predicate"
Expand All @@ -13,7 +11,6 @@ import * as Redacted from "effect/Redacted"
import * as Schema from "effect/Schema"
import * as String from "effect/String"
import type { Mutable } from "effect/Types"

/**
* @since 1.0.0
* @category type ids
Expand Down Expand Up @@ -44,18 +41,24 @@ export interface Headers extends Inspectable.Redactable {
const Proto = Object.assign(Object.create(null), {
[HeadersTypeId]: HeadersTypeId,
[Inspectable.symbolRedactable](
this: Headers,
fiberRefs: FiberRefs.FiberRefs
this: Headers
): Record<string, string | Redacted.Redacted<string>> {
return redact(this, FiberRefs.getOrDefault(fiberRefs, currentRedactedNames))
return redact(this, redactableContext.get(this))
},
[Inspectable.NodeInspectSymbol]() {
return Inspectable.redact(this)
}
})

const make = (input: Record.ReadonlyRecord<string, string>): Mutable<Headers> =>
Object.assign(Object.create(Proto), input) as Headers
const redactableContext = Inspectable.makeRedactableContext((context) => Context.get(context, RedactedNames))

const make = (input: Record.ReadonlyRecord<string, string>, options?: {
readonly redactableContext?: Context.Context<never> | undefined
}): Mutable<Headers> => {
const self = Object.assign(Object.create(Proto), input) as Headers
redactableContext.register(self, options?.redactableContext, input)
return self
}

/**
* @since 1.0.0
Expand Down Expand Up @@ -89,23 +92,29 @@ export type Input =
* @since 1.0.0
* @category constructors
*/
export const empty: Headers = Object.create(Proto)
export const empty = (options?: {
readonly redactableContext?: Context.Context<never> | undefined
}): Headers => make({}, options)

/**
* @since 1.0.0
* @category constructors
*/
export const fromInput: (input?: Input) => Headers = (input) => {
export const fromInput = (input?: Input, options?: {
readonly redactableContext?: Context.Context<never> | undefined
}): Headers => {
if (input === undefined) {
return empty
return empty(options)
} else if (isHeaders(input)) {
return input
} else if (Symbol.iterator in input) {
const out: Record<string, string> = Object.create(Proto)
const out: Record<string, string> = make({}, options)
for (const [k, v] of input) {
out[k.toLowerCase()] = v
}
return out as Headers
}
const out: Record<string, string> = Object.create(Proto)
const out: Record<string, string> = make({}, options)
for (const [k, v] of Object.entries(input)) {
if (Array.isArray(v)) {
out[k.toLowerCase()] = v.join(", ")
Expand All @@ -120,8 +129,13 @@ export const fromInput: (input?: Input) => Headers = (input) => {
* @since 1.0.0
* @category constructors
*/
export const unsafeFromRecord = (input: Record.ReadonlyRecord<string, string>): Headers =>
Object.setPrototypeOf(input, Proto) as Headers
export const unsafeFromRecord = (input: Record.ReadonlyRecord<string, string>, options?: {
readonly redactableContext?: Context.Context<never> | undefined
}): Headers => {
const self = Object.setPrototypeOf(input, Proto)
redactableContext.register(self, options?.redactableContext, input)
return self as Headers
}

/**
* @since 1.0.0
Expand Down Expand Up @@ -173,11 +187,14 @@ export const setAll: {
} = dual<
(headers: Input) => (self: Headers) => Headers,
(self: Headers, headers: Input) => Headers
>(2, (self, headers) =>
make({
>(2, (self, headers) => {
const out = make({
...self,
...fromInput(headers)
}))
})
redactableContext.register(out, undefined, self)
return out
})

/**
* @since 1.0.0
Expand All @@ -192,6 +209,7 @@ export const merge: {
>(2, (self, headers) => {
const out = make(self)
Object.assign(out, headers)
redactableContext.register(out, undefined, self)
return out
})

Expand Down Expand Up @@ -257,15 +275,16 @@ export const redact: {

/**
* @since 1.0.0
* @category fiber refs
* @category references
*/
export const currentRedactedNames: FiberRef.FiberRef<ReadonlyArray<string | RegExp>> = globalValue(
export class RedactedNames extends Context.Reference<RedactedNames>()<
"@effect/platform/Headers/currentRedactedNames",
() =>
FiberRef.unsafeMake<ReadonlyArray<string | RegExp>>([
"authorization",
"cookie",
"set-cookie",
"x-api-key"
])
)
ReadonlyArray<string | RegExp>
>("@effect/platform/Headers/currentRedactedNames", {
defaultValue: () => [
"authorization",
"cookie",
"set-cookie",
"x-api-key"
]
}) {}
2 changes: 1 addition & 1 deletion packages/platform/src/internal/httpClientRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const empty: ClientRequest.HttpClientRequest = makeInternal(
"",
UrlParams.empty,
Option.none(),
Headers.empty,
Headers.empty(),
internalBody.empty
)

Expand Down
Loading

0 comments on commit 552d625

Please sign in to comment.