diff --git a/.changeset/grumpy-plums-grow.md b/.changeset/grumpy-plums-grow.md new file mode 100644 index 00000000000..000cfff1e7c --- /dev/null +++ b/.changeset/grumpy-plums-grow.md @@ -0,0 +1,5 @@ +--- +"@effect/platform": minor +--- + +support non-errors in HttpClient.retryTransient diff --git a/packages/platform/src/HttpClient.ts b/packages/platform/src/HttpClient.ts index 47ac1e211a1..0a8a2f58817 100644 --- a/packages/platform/src/HttpClient.ts +++ b/packages/platform/src/HttpClient.ts @@ -523,20 +523,39 @@ export const retry: { * @category error handling */ export const retryTransient: { - ( + < + B, + E, + R1 = never, + const Mode extends "errors-only" | "response-only" | "both" = never, + Input = "errors-only" extends Mode ? E + : "response-only" extends Mode ? ClientResponse.HttpClientResponse + : ClientResponse.HttpClientResponse | E + >( options: { - readonly while?: Predicate.Predicate> - readonly schedule?: Schedule.Schedule, R1> + readonly mode?: Mode | undefined + readonly while?: Predicate.Predicate> + readonly schedule?: Schedule.Schedule, R1> readonly times?: number - } | Schedule.Schedule, R1> + } | Schedule.Schedule, R1> ): (self: HttpClient.With) => HttpClient.With - ( + < + E, + R, + B, + R1 = never, + const Mode extends "errors-only" | "response-only" | "both" = never, + Input = "errors-only" extends Mode ? E + : "response-only" extends Mode ? ClientResponse.HttpClientResponse + : ClientResponse.HttpClientResponse | E + >( self: HttpClient.With, options: { - readonly while?: Predicate.Predicate> - readonly schedule?: Schedule.Schedule, R1> + readonly mode?: Mode | undefined + readonly while?: Predicate.Predicate> + readonly schedule?: Schedule.Schedule, R1> readonly times?: number - } | Schedule.Schedule, R1> + } | Schedule.Schedule, R1> ): HttpClient.With } = internal.retryTransient diff --git a/packages/platform/src/internal/httpClient.ts b/packages/platform/src/internal/httpClient.ts index a0710a32da7..19b9276f2ab 100644 --- a/packages/platform/src/internal/httpClient.ts +++ b/packages/platform/src/internal/httpClient.ts @@ -3,7 +3,7 @@ import * as Context from "effect/Context" import * as Effect from "effect/Effect" import type * as Fiber from "effect/Fiber" import * as FiberRef from "effect/FiberRef" -import { constFalse, dual } from "effect/Function" +import { constFalse, dual, flow, identity } from "effect/Function" import { globalValue } from "effect/GlobalValue" import * as Inspectable from "effect/Inspectable" import * as Layer from "effect/Layer" @@ -744,41 +744,76 @@ export const retry: { /** @internal */ export const retryTransient: { - ( + < + B, + E, + R1 = never, + const Mode extends "errors-only" | "response-only" | "both" = never, + Input = "errors-only" extends Mode ? E + : "response-only" extends Mode ? ClientResponse.HttpClientResponse + : ClientResponse.HttpClientResponse | E + >( options: { - readonly while?: Predicate.Predicate> - readonly schedule?: Schedule.Schedule, R1> + readonly mode?: Mode | undefined + readonly while?: Predicate.Predicate> + readonly schedule?: Schedule.Schedule, R1> readonly times?: number - } | Schedule.Schedule, R1> + } | Schedule.Schedule, R1> ): (self: Client.HttpClient.With) => Client.HttpClient.With - ( + < + E, + R, + B, + R1 = never, + const Mode extends "errors-only" | "response-only" | "both" = never, + Input = "errors-only" extends Mode ? E + : "response-only" extends Mode ? ClientResponse.HttpClientResponse + : ClientResponse.HttpClientResponse | E + >( self: Client.HttpClient.With, options: { - readonly while?: Predicate.Predicate> - readonly schedule?: Schedule.Schedule, R1> + readonly mode?: Mode | undefined + readonly while?: Predicate.Predicate> + readonly schedule?: Schedule.Schedule, R1> readonly times?: number - } | Schedule.Schedule, R1> + } | Schedule.Schedule, R1> ): Client.HttpClient.With } = dual( 2, ( self: Client.HttpClient.With, options: { - readonly while?: Predicate.Predicate> - readonly schedule?: Schedule.Schedule, R1> + readonly mode?: "errors-only" | "response-only" | "both" | undefined + readonly while?: Predicate.Predicate> + readonly schedule?: Schedule.Schedule, R1> readonly times?: number - } | Schedule.Schedule, R1> - ): Client.HttpClient.With => - transformResponse( + } | Schedule.Schedule, R1> + ): Client.HttpClient.With => { + const isOnlySchedule = Schedule.ScheduleTypeId in options + const mode = isOnlySchedule ? "both" : options.mode ?? "both" + const schedule = isOnlySchedule ? options : options.schedule + const passthroughSchedule = schedule && Schedule.passthrough(schedule) + const times = isOnlySchedule ? undefined : options.times + return transformResponse( self, - Effect.retry({ - while: Schedule.ScheduleTypeId in options || options.while === undefined - ? isTransientError - : Predicate.or(isTransientError, options.while), - schedule: Schedule.ScheduleTypeId in options ? options : options.schedule, - times: Schedule.ScheduleTypeId in options ? undefined : options.times - }) + flow( + mode === "errors-only" ? identity : Effect.repeat({ + schedule: passthroughSchedule, + times, + while: isOnlySchedule || options.while === undefined + ? isTransientResponse + : Predicate.and(isTransientResponse, options.while) + }), + mode === "response-only" ? identity : Effect.retry({ + while: isOnlySchedule || options.while === undefined + ? isTransientError + : Predicate.or(isTransientError, options.while), + schedule, + times + }) + ) ) + } ) const isTransientError = (error: unknown) => @@ -787,7 +822,9 @@ const isTransientError = (error: unknown) => const isTransientHttpError = (error: unknown) => Error.isHttpClientError(error) && ((error._tag === "RequestError" && error.reason === "Transport") || - (error._tag === "ResponseError" && error.response.status >= 429)) + (error._tag === "ResponseError" && isTransientResponse(error.response))) + +const isTransientResponse = (response: ClientResponse.HttpClientResponse) => response.status >= 429 /** @internal */ export const tap = dual<