effect-cf 0.20.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.20.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#67](https://github.com/danieljvdm/effect-cf/pull/67) [`d8e1d64`](https://github.com/danieljvdm/effect-cf/commit/d8e1d64e8117fd8b86fabef6a61de01251557d2b) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Route `Worker.make` fetch handlers through `HttpEffect.toHandled`, matching the first-party Effect HTTP adapters.
8
+
9
+ - Pre-response handlers (`HttpEffect.appendPreResponseHandler`, and things built on it such as `HttpApiBuilder.securitySetCookie`) now run before `HttpServerResponse` results are rendered. Previously they were silently dropped, which broke cookie-based auth flows on Workers.
10
+ - Streaming response bodies keep request-scoped resources alive until the stream completes, instead of finalizing them as soon as the `Response` is returned.
11
+ - `HttpServerResponse` bodies are suppressed for `HEAD` requests.
12
+ - Handler failures are now rendered as HTTP error responses (with the cause reported) instead of rejecting the `fetch` promise. Note that exceptions no longer escape `fetch`, so `passThroughOnException` will not trigger origin fallback for Effect-level failures.
13
+ - Native `Response` values returned from a fetch handler continue to bypass all response processing, including pre-response handlers — WebSocket upgrade responses and app-level `HttpEffect.toHandled` wrappers pass through untouched.
14
+
3
15
  ## 0.20.0
4
16
 
5
17
  ### Minor Changes
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { Cause, Clock, Config, ConfigProvider, Context, Data, DateTime, Duration, Effect, Exit, Layer, ManagedRuntime, Option, Queue, Result, Schema } from "effect";
3
- import { FetchHttpClient, HttpClient, HttpClientRequest, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
3
+ import { FetchHttpClient, HttpClient, HttpClientRequest, HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
4
4
  import { OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
5
5
  import { D1Client } from "@effect/sql-d1";
6
6
  import { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, WorkflowEntrypoint } from "cloudflare:workers";
@@ -1874,7 +1874,27 @@ const reservedMethodNames$2 = /* @__PURE__ */ new Set([
1874
1874
  ]);
1875
1875
  const RunSymbol$1 = Symbol.for("effect-cf/Worker/run");
1876
1876
  const renderHttpResponse = (effect) => Effect.flatMap(effect, (response) => Effect.map(Effect.context(), (context) => HttpServerResponse.toWeb(response, { context })));
1877
- const renderFetchSuccess = (effect) => Effect.flatMap(effect, (response) => response instanceof Response ? Effect.succeed(response) : Effect.map(Effect.context(), (context) => HttpServerResponse.toWeb(response, { context })));
1877
+ const renderFetchSuccess = (effect) => Effect.suspend(() => {
1878
+ let nativeResponse;
1879
+ let webResponse;
1880
+ const handled = HttpEffect.toHandled(Effect.flatMap(effect, (response) => {
1881
+ if (response instanceof Response) {
1882
+ nativeResponse = response;
1883
+ return Effect.succeed(HttpServerResponse.empty());
1884
+ }
1885
+ return Effect.succeed(response);
1886
+ }), (request, response) => nativeResponse !== void 0 ? Effect.void : Effect.map(Effect.context(), (context) => {
1887
+ webResponse = HttpServerResponse.toWeb(HttpEffect.scopeTransferToStream(response), {
1888
+ withoutBody: request.method === "HEAD",
1889
+ context
1890
+ });
1891
+ }));
1892
+ const captured = () => nativeResponse ?? webResponse ?? new Response(null, { status: 500 });
1893
+ return Effect.matchCause(handled, {
1894
+ onFailure: captured,
1895
+ onSuccess: captured
1896
+ });
1897
+ });
1878
1898
  const isWorkerOptions = (options) => typeof options === "object" && options !== null && ("eventLayer" in options || "fetch" in options || "queue" in options || "rpc" in options);
1879
1899
  function make$9(layer, optionsOrFetch) {
1880
1900
  const options = isWorkerOptions(optionsOrFetch) ? optionsOrFetch : { fetch: optionsOrFetch };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/Worker.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  type Schema as S,
10
10
  type Scope,
11
11
  } from "effect";
12
- import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
12
+ import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
13
13
 
14
14
  import type * as Binding from "./Binding";
15
15
  import { WorkerConfig, WorkerEnvironment, type WorkerEnv } from "./Environment";
@@ -196,14 +196,41 @@ export const renderHttpResponse = <A extends HttpServerResponse.HttpServerRespon
196
196
 
197
197
  const renderFetchSuccess = <E, R>(
198
198
  effect: Effect.Effect<WorkerFetchSuccess, E, R>,
199
- ): Effect.Effect<Response, E, R> =>
200
- Effect.flatMap(effect, (response) =>
201
- response instanceof Response
202
- ? Effect.succeed(response)
203
- : Effect.map(Effect.context<never>(), (context) =>
204
- HttpServerResponse.toWeb(response, { context }),
205
- ),
206
- );
199
+ ): Effect.Effect<Response, E, Exclude<R, Scope.Scope> | HttpServerRequest.HttpServerRequest> =>
200
+ Effect.suspend(() => {
201
+ // Native `Response` values bypass all HTTP response processing (including
202
+ // pre-response handlers), so app-level `HttpEffect.toHandled` wrappers and
203
+ // WebSocket upgrade responses pass through untouched.
204
+ let nativeResponse: Response | undefined;
205
+ let webResponse: Response | undefined;
206
+
207
+ const handled = HttpEffect.toHandled(
208
+ Effect.flatMap(effect, (response) => {
209
+ if (response instanceof Response) {
210
+ nativeResponse = response;
211
+
212
+ return Effect.succeed(HttpServerResponse.empty());
213
+ }
214
+
215
+ return Effect.succeed(response);
216
+ }),
217
+ (request, response) =>
218
+ nativeResponse !== undefined
219
+ ? Effect.void
220
+ : Effect.map(Effect.context<never>(), (context) => {
221
+ webResponse = HttpServerResponse.toWeb(HttpEffect.scopeTransferToStream(response), {
222
+ withoutBody: request.method === "HEAD",
223
+ context,
224
+ });
225
+ }),
226
+ );
227
+
228
+ // `toHandled` re-fails with the original cause after rendering the error
229
+ // response, so both branches return the response captured above.
230
+ const captured = () => nativeResponse ?? webResponse ?? new Response(null, { status: 500 });
231
+
232
+ return Effect.matchCause(handled, { onFailure: captured, onSuccess: captured });
233
+ });
207
234
 
208
235
  const isWorkerOptions = <ROut, REvent, EventLayerError, Rpc extends WorkerRpc<ROut | REvent>>(
209
236
  options: WorkerOptions<ROut, REvent, EventLayerError, Rpc> | WorkerHandler<ROut>,