@@ -2,11 +2,15 @@ import * as Cause from "effect/Cause";
22import * as Config from "effect/Config" ;
33import * as Context from "effect/Context" ;
44import * as Effect from "effect/Effect" ;
5+ import * as ErrorReporter from "effect/ErrorReporter" ;
56import * as Layer from "effect/Layer" ;
67import * as Option from "effect/Option" ;
78import type { Scope } from "effect/Scope" ;
89import type { HttpBodyError } from "effect/unstable/http/HttpBody" ;
9- import type { HttpServerError } from "effect/unstable/http/HttpServerError" ;
10+ import {
11+ causeResponse ,
12+ type HttpServerError ,
13+ } from "effect/unstable/http/HttpServerError" ;
1014import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest" ;
1115import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse" ;
1216
@@ -82,26 +86,45 @@ export const safeHttpEffect = <Req = never>(
8286 : response ,
8387 ) ,
8488 ) as any as HttpEffect < Req > ,
85- ( cause ) => {
86- // ClientAbort interrupts are not real failures — the client closed
87- // the connection. Skip logging and respond with 499 if applicable.
88- if ( Cause . hasInterruptsOnly ( cause ) ) {
89- return Effect . succeed ( HttpServerResponse . empty ( { status : 499 } ) ) ;
90- }
91- // Log the full cause server-side so operators can debug, but return
92- // a generic 500 to the client. Causes can contain sensitive data
93- // (prompt contents, API keys baked into error messages, internal
94- // file paths) and should never be echoed back to the network.
95- return Effect . logError ( "HTTP handler failed" , cause ) . pipe (
96- Effect . as (
97- HttpServerResponse . text ( "Internal Server Error" , {
98- status : 500 ,
99- statusText : "Internal Server Error" ,
100- } ) ,
89+ ( cause ) =>
90+ // `causeResponse` is effect's native failure boundary: Respondable
91+ // failures keep their intended response (e.g. RouteNotFound -> 404),
92+ // client aborts map to 499, and everything else becomes an empty 500 —
93+ // the cause is never echoed to the network, as it can contain sensitive
94+ // data (prompt contents, API keys baked into error messages, internal
95+ // file paths).
96+ causeResponse ( cause ) . pipe (
97+ Effect . flatMap ( ( [ response , reportableCause ] ) =>
98+ Effect . withFiber ( ( fiber ) =>
99+ fiber . getRef ( ErrorReporter . CurrentErrorReporters ) . size > 0
100+ ? ErrorReporter . report ( reportableCause )
101+ : logUnreportedCause ( reportableCause ) ,
102+ ) . pipe ( Effect . as ( response ) ) ,
101103 ) ,
102- ) ;
103- } ,
104+ ) ,
105+ ) ;
106+
107+ /**
108+ * No `ErrorReporter` is registered by default, so without a fallback a defect
109+ * in a deployed Function/Worker would produce a bare 500 and vanish without a
110+ * trace. Log the cause server-side so operators can debug, applying the same
111+ * filtering `ErrorReporter.make` reporters do: interrupts (client aborts) and
112+ * `ErrorReporter.ignore`-annotated values (Respondable errors like
113+ * RouteNotFound, and the response `causeResponse` appends) are not failures
114+ * and are skipped.
115+ */
116+ const logUnreportedCause = ( cause : Cause . Cause < unknown > ) => {
117+ const failures = cause . reasons . filter (
118+ ( reason ) =>
119+ reason . _tag !== "Interrupt" &&
120+ ! ErrorReporter . isIgnored (
121+ reason . _tag === "Fail" ? reason . error : reason . defect ,
122+ ) ,
104123 ) ;
124+ return failures . length === 0
125+ ? Effect . void
126+ : Effect . logError ( "HTTP handler failed" , Cause . fromReasons ( failures ) ) ;
127+ } ;
105128
106129export const resolvePort = ( options : { port ?: number } | undefined ) =>
107130 options ?. port !== undefined
0 commit comments