Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/http-export-transient-predicates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": minor
---

Export `HttpClient.isTransientError` and `HttpClient.isTransientResponse`, the predicates `retryTransient` uses to decide what to retry.
49 changes: 47 additions & 2 deletions packages/effect/src/http/HttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1916,7 +1916,38 @@ class InterruptibleResponse implements HttpClientResponse.HttpClientResponse, Pi
}
}

const isTransientError = (error: unknown) => Cause.isTimeoutError(error) || isTransientHttpError(error)
/**
* Returns `true` for an error that `retryTransient` retries by default: a
* timeout, a transport error, or a status code error with a transient response.
*
* **When to use**
*
* Use to classify failures the same way `retryTransient` does, for example in a
* custom retry policy or a circuit breaker.
*
* **Example** (Classifying client errors)
*
* ```ts import.meta.vitest
* import { Cause } from "effect"
* import { HttpClient, HttpClientError, HttpClientRequest } from "effect/http"
*
* const request = HttpClientRequest.get("https://example.com")
* const transportError = new HttpClientError.HttpClientError({
* reason: new HttpClientError.TransportError({ request })
* })
*
* HttpClient.isTransientError(transportError) // => true
* HttpClient.isTransientError(new Cause.TimeoutError()) // => true
* HttpClient.isTransientError(new Error("boom")) // => false
* ```
*
* @see {@link isTransientResponse} for the response statuses it accepts
* @see {@link retryTransient} for the retry policy that uses it
* @stability unstable
* @category predicates
* @since 4.1.0
*/
export const isTransientError = (error: unknown): boolean => Cause.isTimeoutError(error) || isTransientHttpError(error)

const isTransientHttpError = (error: unknown) =>
Error.isHttpClientError(error) &&
Expand All @@ -1928,7 +1959,21 @@ const isTooManyRequestsHttpClientError = (
): error is Error.HttpClientError & { readonly reason: Error.StatusCodeError } =>
Error.isHttpClientError(error) && error.reason._tag === "StatusCodeError" && error.reason.response.status === 429

const isTransientResponse = (response: HttpClientResponse.HttpClientResponse) =>
/**
* Returns `true` for a response whose status `retryTransient` retries by
* default: 408, 429, 500, 502, 503 or 504.
*
* **When to use**
*
* Use to retry the same responses as `retryTransient` in a custom retry policy.
*
* @see {@link isTransientError} for the matching error predicate
* @see {@link retryTransient} for the retry policy that uses it
* @stability unstable
* @category predicates
* @since 4.1.0
*/
export const isTransientResponse = (response: HttpClientResponse.HttpClientResponse): boolean =>
response.status === 408 ||
response.status === 429 ||
response.status === 500 ||
Expand Down
26 changes: 24 additions & 2 deletions packages/effect/test/http/HttpClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { assert, describe, it, vi } from "@effect/vitest"
import { strictEqual } from "@effect/vitest/utils"
import { Clock, Deferred, Duration, Effect, Fiber, Layer, Ref, Schema, Stream } from "effect"
import { Cookies, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"
import { Cause, Clock, Deferred, Duration, Effect, Fiber, Layer, Ref, Schema, Stream } from "effect"
import { Cookies, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http"
import { RateLimiter } from "effect/persistence"
import { TestClock } from "effect/testing"
import * as Tracer from "effect/Tracer"
Expand Down Expand Up @@ -397,6 +397,28 @@ Missing key
}))
})

it("classifies transient responses and errors", () => {
const request = HttpClientRequest.get("http://test/")
const responses = [200, 404, 408, 429, 500, 501, 502, 503, 504].map((status) =>
HttpClientResponse.fromWeb(request, new Response(null, { status }))
)
const statusErrors = responses.map((response) =>
new HttpClientError.HttpClientError({ reason: new HttpClientError.StatusCodeError({ request, response }) })
)
const expected = [false, false, true, true, true, false, true, true, true]
assert.deepStrictEqual(responses.map(HttpClient.isTransientResponse), expected)
assert.deepStrictEqual(statusErrors.map(HttpClient.isTransientError), expected)
assert.deepStrictEqual(
[
new Cause.TimeoutError(),
new HttpClientError.HttpClientError({ reason: new HttpClientError.TransportError({ request }) }),
new HttpClientError.HttpClientError({ reason: new HttpClientError.EncodeError({ request }) }),
new Error("boom")
].map(HttpClient.isTransientError),
[true, true, false, false]
)
})

describe("retryTransient", () => {
it.effect("retries transient responses with retryOn errors-and-responses", () =>
Effect.gen(function*() {
Expand Down
Loading