Repository F# setup
open System
open System.Net.Http
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.HttpClient
open Axial.PlatformServiceReliability
Per-Request Timeouts
HttpClient.Timeout is one global setting that throws TaskCanceledException, indistinguishable from real
cancellation. An Axial timeout is per request and produces a dedicated typed error:
The examples on this page run against a scripted service that replies with fixed statuses in turn and records each
request, so they can check how many attempts were made. The double is the ScriptedHttp from the
testing guide:
Show page setup
// Setup for the checked examples on this page.
open System
open System.Net.Http
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.HttpClient
open Axial.PlatformService
/// Fails the docs test when an example's result differs from the value shown.
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actualopen Axial.HttpClient.DSL
type TestEnv =
{ Http: IHttp; Clock: IClock }
interface IHasHttp with
member this.Http = this.Http
interface IHasClock with
member this.Clock = this.Clock
/// An HTTP service that replies with each status and body in turn, and records the requests it received.
type ScriptedHttp(replies: (int * string) list) =
let remaining = Collections.Generic.Queue(replies)
let sent = ResizeArray<HttpRequest>()
member _.Sent = List.ofSeq sent
member this.Env = { Http = this; Clock = Clock.live }
interface IHttp with
member _.Send(request) =
Flow.delay (fun () ->
sent.Add request
let status, body = remaining.Dequeue()
Flow.succeed (Response.create (DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)) status body))
type User = { Id: int; Name: string }
/// A hand-written decoder for {"id":1,"name":"Ada"}; a JSON library's decoder fits the same signature.
let decodeUser (json: string) : Result<User, string> =
let found = Text.RegularExpressions.Regex.Match(json, """^\{"id":(\d+),"name":"([^"]*)"\}$""")
if found.Success then Ok { Id = int found.Groups[1].Value; Name = found.Groups[2].Value }
else Error $"not a user: {json}"
AxialHttpClientAxial.HttpClient.DSLFsLiveDocsGeneratedPage2_03F7AABF2A94.TestEnvHttp: IHttpAxial.HttpClient.IHttpSends fully described HTTP requests for a concrete host platform.
Clock: IClockAxial.IClockSupplies wall time, monotonic time, and cancellable delays from one source. Wall time is for timestamps. Use differences between Elapsed readings for durations.
Axial.HttpClient.IHasHttpDeclares that an environment supplies the HTTP service. Implement this on the environment supplied at the host edge. A workflow that sends requests constrains its environment with 'env :> IHasHttp.
this: TestEnvHttp: TestEnv -> unit -> IHttpAxial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
Clock: TestEnv -> unit -> IClockFsLiveDocsGeneratedPage2_03F7AABF2A94.ScriptedHttpreplies: (int * string) listintAn abbreviation for the CLI type . Basic Types
stringAn abbreviation for the CLI type . Basic Types
listThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
remaining: Collections.Generic.Queue<int * string>Collections``.ctor``: (int * string) seq -> unitInitializes a new instance of the class that contains elements copied from the specified collection and has sufficient capacity to accommodate the number of elements copied. The collection whose elements are copied to the new . is .
Genericsent: ResizeArray<HttpRequest>``.ctor``: unit -> unitInitializes a new instance of the class that is empty and has the default initial capacity.
Axial.HttpClient.HttpRequestAn immutable, fully described HTTP request.
_: ScriptedHttpSent: ScriptedHttp -> unit -> HttpRequest listMicrosoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
ofSeq: 'T seq -> 'T listBuilds a new list from the given enumerable object. The input sequence. The list of elements from the sequence. let inputs = seq { 1; 2; 5 } inputs |> List.ofSeq Evaluates to [ 1; 2; 5 ]. This is an O(n) operation, where n is the length of the sequence.
this: ScriptedHttpEnv: ScriptedHttp -> unit -> TestEnvAxial.PlatformService.ClockHelpers for the clock service.
live: IClockCreates a live clock backed by and a monotonic timer.
Send: ScriptedHttp -> HttpRequest -> Flow<unit,HttpError,HttpResponse>request: HttpRequestAxial.Flowdelay: (unit -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Defers flow construction until execution time. A function that returns the flow to execute. Recursive calls deferred here are consumed by the executor without growing a chain of parent tasks. Work that must run after the recursive call still needs storage until that call finishes. A flow that lazily evaluates the factory when executed. let flow = Flow.delay (fun () -> Flow.succeed 42)
Add: HttpRequest -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
status: intbody: stringDequeue: unit -> int * stringRemoves and returns the object at the beginning of the . The object that is removed from the beginning of the . The is empty.
succeed: 'value -> Flow<'env,'error,'value>Same as ok. The value to wrap in a successful flow. A flow that always succeeds with the provided value. let result = Flow.succeed 42 |> Flow.run () // result = Success 42
Axial.HttpClient.ResponseModulecreate: DateTimeOffset -> int -> string -> HttpResponseCreates a synthetic response transcript at an explicit start time, primarily for test fakes. <example><code>Response.create startedAt 200 """{"ok":true}"""</code></example>
``.ctor``: int * int * int * int * int * int * TimeSpan -> unitInitializes a new instance of the structure using the specified year, month, day, hour, minute, second, and offset. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ). The hours (0 through 23). The minutes (0 through 59). The seconds (0 through 59). The time's offset from Coordinated Universal Time (UTC). does not represent whole minutes. is less than one or greater than 9999. -or- is less than one or greater than 12. -or- is less than one or greater than the number of days in . -or- is less than zero or greater than 23. -or- is less than 0 or greater than 59. -or- is less than 0 or greater than 59. -or- is less than -14 hours or greater than 14 hours. -or- The property is earlier than or later than .
System.TimeSpanRepresents a time interval.
Zero: TimeSpanRepresents the zero value. This field is read-only.
FsLiveDocsGeneratedPage2_03F7AABF2A94.UserId: intName: stringdecodeUser: string -> Result<User,string>json: stringMicrosoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
found: Text.RegularExpressions.MatchTextMatch: string * string -> Text.RegularExpressions.MatchSearches the specified input string for the first occurrence of the specified regular expression. The string to search for a match. The regular expression pattern to match. An object that contains information about the match. A regular expression parsing error occurred. or is . A time-out occurred. For more information about time-outs, see the Remarks section.
RegularExpressionsSystem.Text.RegularExpressions.RegexRepresents an immutable regular expression.
Success: boolGets a value indicating whether the match is successful. if the match is successful; otherwise, .
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
int: ^T -> intConverts the argument to signed 32-bit integer. This is a direct conversion for all primitive numeric types. For strings, the input is converted using Int32.Parse() with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type. The input value. The converted int int 'A' // evaluates to 65 int 0xff // evaluates to 255 int -10 // evaluates to -10
Groups: Text.RegularExpressions.GroupCollectionGets a collection of groups matched by the regular expression. The character groups matched by the pattern.
Item: Text.RegularExpressions.GroupEnables access to a member of the collection by integer index. The zero-based index of the collection member to be retrieved. The member of the collection specified by .
Value: stringGets the captured substring from the input string. The substring that is captured by the match.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
Requires & types
Requires: open Axial; open Axial.HttpClient; open Axial.PlatformService; open System (packages: Axial, Axial.HttpClient, Axial.PlatformService)val decodeUser: string -> Result<User,string>let slowReport : Flow<TestEnv, HttpError, HttpResponse> =
GET $"https://api.example.com/slow-report"
|> timeout (TimeSpan.FromSeconds 5.0)
|> fetch
slowReport: Flow<TestEnv,HttpError,HttpResponse>Axial.Flow`3FsLiveDocsGeneratedPage2_03F7AABF2A94.TestEnvAxial.HttpClient.HttpErrorA recoverable HTTP transport, timeout, status, or decoding failure.
Axial.HttpClient.HttpResponseThe complete response transcript for one HTTP exchange.
GET: FormattableString -> HttpRequestCreates a GET request from an interpolated URL. Every hole is URL-encoded as one value. <example><code>GET $"https://api.example.com/users/{userId}"</code></example>
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
timeout: TimeSpan -> HttpRequest -> HttpRequestSets a per-request timeout. <example><code>request |> timeout (TimeSpan.FromSeconds 5.0)</code></example>
System.TimeSpanRepresents a time interval.
FromSeconds: float -> TimeSpanReturns a that represents a specified number of seconds, where the specification is accurate to the nearest millisecond. A number of seconds, accurate to the nearest millisecond. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
fetch: HttpRequest -> Flow<'a,HttpError,HttpResponse>Sends the request and returns the full transcript, failing on unexpected statuses. <example><code>GET $"{root}/users" |> fetch</code></example>
Requires & types
Requires: open Axial; open Axial.HttpClient; open Axial.HttpClient.DSL; open System (packages: Axial, Axial.HttpClient)val slowReport: Flow<TestEnv,HttpError,HttpResponse>A handler that accepts the connection and never responds leaves only the per-request timeout to finish the call:
let stalledHandler () =
{ new HttpMessageHandler() with
member _.SendAsync(request: HttpRequestMessage, cancellationToken: CancellationToken) : Task<HttpResponseMessage> =
task {
do! Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken)
return null
} }
let timeoutEnv =
{ Http = Http.live Clock.live (new HttpClient(stalledHandler ()))
Clock = Clock.live }
stalledHandler: unit -> HttpMessageHandlerSystem.Net.Http.HttpMessageHandlerA base type for HTTP message handlers.
_: HttpMessageHandlerSendAsync: HttpRequestMessage * CancellationToken -> Task<HttpResponseMessage>Send an HTTP request as an asynchronous operation. The HTTP request message to send. The cancellation token to cancel operation. The task object representing the asynchronous operation. The was .
request: HttpRequestMessageSystem.Net.Http.HttpRequestMessageRepresents a HTTP request message.
cancellationToken: CancellationTokenSystem.Threading.CancellationTokenPropagates notification that operations should be canceled.
System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
System.Net.Http.HttpResponseMessageRepresents a HTTP response message including the status code and data.
task: TaskBuilderBuilds a task using computation expression syntax.
System.Threading.Tasks.TaskRepresents an asynchronous operation.
Delay: TimeSpan * CancellationToken -> TaskCreates a cancellable task that completes after a specified time interval. The time span to wait before completing the returned task, or to wait indefinitely. A cancellation token to observe while waiting for the task to complete. A task that represents the time delay. represents a negative time interval other than . -or- The argument's property is greater than . The task has been canceled. The provided has already been disposed.
System.Threading.TimeoutContains constants that specify infinite time-out intervals. This class cannot be inherited.
InfiniteTimeSpan: TimeSpanA constant used to specify an infinite waiting period, for methods that accept a parameter.
timeoutEnv: TestEnvHttp: IHttpAxial.HttpClient.HttpModulelive: IClock -> HttpClient -> IHttpCreates a live HTTP service backed by an explicit clock and <see cref="T:System.Net.Http.HttpClient" />. <example><code>Http.live Clock.live (new HttpClient())</code></example>
Axial.PlatformService.ClockHelpers for the clock service.
live: IClockCreates a live clock backed by and a monotonic timer.
System.Net.Http.HttpClientProvides a base class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.
Clock: IClockRequires & types
Requires: open Axial.HttpClient; open Axial.PlatformService; open System.Net.Http; open System.Threading; open System.Threading.Tasks (packages: Axial.HttpClient, Axial.PlatformService)val stalledHandler: unit -> HttpMessageHandlerval timeoutEnv: TestEnvlet shortTimeout =
GET $"https://api.example.com/slow-report"
|> timeout (TimeSpan.FromMilliseconds 50.0)
|> fetch
match shortTimeout |> Flow.run timeoutEnv with
| Exit.Failure(Cause.Fail(HttpError.TimedOut(request, timeout))) ->
timeout |> shouldEqual (TimeSpan.FromMilliseconds 50.0)
request |> shouldEqual "GET https://api.example.com/slow-report"
| other -> failwithf "expected HttpError.TimedOut, got %A" other
shortTimeout: Flow<TestEnv,HttpError,HttpResponse>GET: FormattableString -> HttpRequestCreates a GET request from an interpolated URL. Every hole is URL-encoded as one value. <example><code>GET $"https://api.example.com/users/{userId}"</code></example>
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
timeout: TimeSpan -> HttpRequest -> HttpRequestSets a per-request timeout. <example><code>request |> timeout (TimeSpan.FromSeconds 5.0)</code></example>
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
fetch: HttpRequest -> Flow<'a,HttpError,HttpResponse>Sends the request and returns the full transcript, failing on unexpected statuses. <example><code>GET $"{root}/users" |> fetch</code></example>
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
timeoutEnv: TestEnvAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
FailureThe workflow failed due to a specific cause.
Axial.Cause`1Represents the cause of a failed workflow. The type of the domain-specific failure value.
FailAn expected domain-specific failure.
Axial.HttpClient.HttpErrorA recoverable HTTP transport, timeout, status, or decoding failure.
TimedOutThe per-request timeout elapsed before the response completed.
request: stringtimeout: TimeSpanshouldEqual: 'a -> 'a -> unitother: Exit<HttpResponse,HttpError>failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
Requires & types
Requires: open Axial; open Axial.HttpClient; open Axial.HttpClient.DSL; open System (packages: Axial, Axial.HttpClient)val shortTimeout: Flow<TestEnv,HttpError,HttpResponse>The checked block uses a 50ms deadline so the docs test does not sleep; slowReport keeps the readable 5s value.
The TimedOut case carries the redacted request line and the timeout that elapsed.
Interrupting the workflow is Cause.Interrupt, never HttpError.TimedOut; the runtime token is the one that asked,
so the failure stays outside the typed error channel:
let interruptedSource = new CancellationTokenSource()
interruptedSource.Cancel()
let interruptedTimeout =
GET $"https://api.example.com/slow-report"
|> timeout (TimeSpan.FromMilliseconds 50.0)
|> fetch
match interruptedTimeout.RunSynchronously(timeoutEnv, interruptedSource.Token) with
| Exit.Failure Cause.Interrupt -> ()
| other -> failwithf "expected Cause.Interrupt, got %A" other
interruptedSource: CancellationTokenSourceSystem.Threading.CancellationTokenSourceSignals to a that it should be canceled.
Cancel: unit -> unitCommunicates a request for cancellation. This has been disposed. An aggregate exception containing all the exceptions thrown by the registered callbacks on the associated .
interruptedTimeout: Flow<TestEnv,HttpError,HttpResponse>GET: FormattableString -> HttpRequestCreates a GET request from an interpolated URL. Every hole is URL-encoded as one value. <example><code>GET $"https://api.example.com/users/{userId}"</code></example>
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
timeout: TimeSpan -> HttpRequest -> HttpRequestSets a per-request timeout. <example><code>request |> timeout (TimeSpan.FromSeconds 5.0)</code></example>
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
fetch: HttpRequest -> Flow<'a,HttpError,HttpResponse>Sends the request and returns the full transcript, failing on unexpected statuses. <example><code>GET $"{root}/users" |> fetch</code></example>
RunSynchronously: TestEnv * CancellationToken option -> Exit<HttpResponse,HttpError>Starts the workflow and blocks until the final exit is available. To bound how long the workflow may run, apply Flow.timeout to it: that interrupts the work and waits for its cleanup, where abandoning a blocked wait would leave it running. The environment used by the workflow. The optional cancellation token. Defaults to . The final workflow exit. .NET only
timeoutEnv: TestEnvToken: CancellationTokenGets the associated with this . The associated with this . The token source has been disposed.
Axial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
FailureThe workflow failed due to a specific cause.
Axial.Cause`1Represents the cause of a failed workflow. The type of the domain-specific failure value.
InterruptAn administrative signal to stop the workflow (e.g., cancellation).
other: Exit<HttpResponse,HttpError>failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
Requires & types
Requires: open Axial; open Axial.HttpClient.DSL; open System; open System.Threading (packages: Axial, Axial.HttpClient)val interruptedSource: CancellationTokenSourceval interruptedTimeout: Flow<TestEnv,HttpError,HttpResponse>The live service enforces the timeout with a linked cancellation source, so the connection is torn down when the deadline passes, not merely abandoned.
Retry Only Transient Failures
Retrying a 404 or a decode failure wastes time and can duplicate side effects. HttpError.isTransient
classifies exactly the failures where a retry can help: connection failures, timeouts, and 408/429/5xx statuses.
let firstUser : Flow<TestEnv, HttpError, User> =
Http.getJson decodeUser "https://api.example.com/users/1"
|> Http.retryTransient 3 (TimeSpan.FromMilliseconds 1.0)
firstUser: Flow<TestEnv,HttpError,User>Axial.Flow`3FsLiveDocsGeneratedPage2_03F7AABF2A94.TestEnvAxial.HttpClient.HttpErrorA recoverable HTTP transport, timeout, status, or decoding failure.
FsLiveDocsGeneratedPage2_03F7AABF2A94.UserAxial.HttpClient.HttpModulegetJson: (string -> Result<'value,string>) -> string -> Flow<'env,HttpError,'value>Sends a GET request and decodes the JSON response. <example><code>Http.getJson (Json.deserializeResult codec) "https://api.example.com/users/1"</code></example>
decodeUser: string -> Result<User,string>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
retryTransient: int -> TimeSpan -> Flow<'env,HttpError,'value> -> Flow<'env,HttpError,'value>Retries a workflow on transient HTTP errors with exponential backoff. Permanent failures such as 404 or decode errors are never retried. <example><code>Http.getJson decode url |> Http.retryTransient 3 (TimeSpan.FromMilliseconds 200.0)</code></example>
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
Requires & types
Requires: open Axial; open Axial.HttpClient; open System (packages: Axial, Axial.HttpClient)val firstUser: Flow<TestEnv,HttpError,User>Two 503 replies are transient, so the third attempt succeeds:
let flaky = ScriptedHttp([ 503, ""; 503, ""; 200, """{"id":1,"name":"Ada"}""" ])
firstUser |> Flow.run flaky.Env |> shouldEqual (Exit.Success { Id = 1; Name = "Ada" })
flaky.Sent.Length |> shouldEqual 3
flaky: ScriptedHttp``.ctor``: (int * string) list -> ScriptedHttpfirstUser: Flow<TestEnv,HttpError,User>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
Env: TestEnvshouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Id: intName: stringLength: intGets the number of items contained in the list
Sent: HttpRequest listRequires & types
Requires: open Axial (packages: Axial)val flaky: ScriptedHttpretryTransient 3 makes up to 3 retries after the first attempt. The example above passes a 1ms base delay so
the docs test does not sleep, producing a 1ms, 2ms, 4ms schedule. The DSL shorthand withRetries 3 applies the
same exponential schedule with the 200ms base delay instead, so its retries wait 200ms, 400ms, then 800ms.
A permanent failure such as HttpError.Status 404 or HttpError.DecodeFailed fails immediately on the first
attempt. To combine the retry with other schedules, take the schedule itself:
HttpError.transientRetry 3 (TimeSpan.FromMilliseconds 200.0) is the Schedule that retryTransient passes to
Flow.retry.
For full control, describe the retry yourself and pass it to Flow.retry. A Retry record names each choice:
let notRateLimited error =
HttpError.isTransient error
&& (match error with HttpError.Status r -> r.StatusCode <> 429 | _ -> true)
let patientUser : Flow<TestEnv, HttpError, User> =
Http.getJson decodeUser "https://api.example.com/users/1"
|> Flow.retry (Retry.schedule { Retry.defaults with Retries = 5; When = notRateLimited })
notRateLimited: HttpError -> boolerror: HttpErrorAxial.HttpClient.HttpErrorModuleisTransient: HttpError -> boolIndicates whether retrying the same request could plausibly succeed. Connection failures, timeouts, and 408/429/5xx statuses are transient. <example><code>if HttpError.isTransient error then retry ()</code></example>
(&&): bool -> bool -> boolBinary 'and'. When used as a binary operator the right hand value is evaluated only on demand The first value. The second value. The result of the operation.
Axial.HttpClient.HttpErrorA recoverable HTTP transport, timeout, status, or decoding failure.
StatusThe response arrived with a status outside the request's expectation.
r: HttpResponseStatusCode: intThe numeric response status code.
(<>): 'T -> 'T -> boolStructural inequality The first parameter. The second parameter. The result of the comparison. 5 <> 5 // Evaluates to false 5 <> 6 // Evaluates to true [1; 2] <> [1; 2] // Evaluates to false
patientUser: Flow<TestEnv,HttpError,User>Axial.Flow`3FsLiveDocsGeneratedPage2_03F7AABF2A94.TestEnvFsLiveDocsGeneratedPage2_03F7AABF2A94.UserAxial.HttpClient.HttpModulegetJson: (string -> Result<'value,string>) -> string -> Flow<'env,HttpError,'value>Sends a GET request and decodes the JSON response. <example><code>Http.getJson (Json.deserializeResult codec) "https://api.example.com/users/1"</code></example>
decodeUser: string -> Result<User,string>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowretry: Schedule<'env,'error,'output> -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Retries a flow's typed failures according to a schedule. The flow runs once, and after each Cause.Fail the schedule sees the error and decides whether to run it again and how long to wait. Defects and interruptions are never retried. When the schedule stops, the flow fails with the last error. Each attempt runs in its own child scope; a failed attempt's finalizers run before the next attempt starts. For the common case, build the schedule from a Retry record. Decides, from each typed error, whether to retry and after what delay. The flow to retry. A flow that succeeds as soon as an attempt succeeds. fetch |> Flow.retry (Schedule.recurs 3) fetch |> Flow.retry (Retry.schedule { Retry.defaults with When = HttpError.isTransient })
Axial.RetryModuleConstructors for .
schedule: Retry<'input> -> Schedule<'env,'input,int>Builds the schedule a retry record describes. The retry description. A schedule that emits the retry number, starting at 0. Thrown when Retries is negative or a delay is negative.
Axial.Retry`1A retry described with named fields: how many retries, how long to wait, and which inputs to retry. Build one from Retry.defaults with record update syntax and pass it to Flow.retry through Retry.schedule. It is a shorthand for the common case; anything a record cannot express (jitter, elapsed-time limits, fixed-rate runs) is written as a Schedule directly. The input the retry decision sees: the typed error for Flow.retry, the defect for Flow.supervise.
defaults: Retry<'input>Three retries with exponential backoff from 100 ms, capped at 10 s, retrying every input. fetch |> Flow.retry (Retry.schedule { Retry.defaults with Retries = 5; When = HttpError.isTransient })
Retries: intThe maximum number of retries after the first attempt; 3 allows 4 executions in total.
When: 'input -> boolReturns true for inputs that should be retried; others stop immediately.
Requires & types
Requires: open Axial; open Axial.HttpClient (packages: Axial, Axial.HttpClient)val notRateLimited: HttpError -> boolval patientUser: Flow<TestEnv,HttpError,User>A 429 is transient, but this policy leaves it to the caller, so it is not retried:
let limited = ScriptedHttp([ 429, "" ])
match patientUser |> Flow.run limited.Env with
| Exit.Failure(Cause.Fail(HttpError.Status response)) -> response.StatusCode |> shouldEqual 429
| other -> failwithf "expected a 429, got %A" other
limited.Sent.Length |> shouldEqual 1
limited: ScriptedHttp``.ctor``: (int * string) list -> ScriptedHttppatientUser: Flow<TestEnv,HttpError,User>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
Env: TestEnvAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
FailureThe workflow failed due to a specific cause.
Axial.Cause`1Represents the cause of a failed workflow. The type of the domain-specific failure value.
FailAn expected domain-specific failure.
Axial.HttpClient.HttpErrorA recoverable HTTP transport, timeout, status, or decoding failure.
StatusThe response arrived with a status outside the request's expectation.
response: HttpResponseStatusCode: intThe numeric response status code.
shouldEqual: 'a -> 'a -> unitother: Exit<User,HttpError>failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
Length: intGets the number of items contained in the list
Sent: HttpRequest listRequires & types
Requires: open Axial; open Axial.HttpClient (packages: Axial, Axial.HttpClient)val limited: ScriptedHttpThe same retry as a Schedule pipeline adds what a record cannot express, such as jitter:
let jitteredUser : Flow<TestEnv, HttpError, User> =
Http.getJson decodeUser "https://api.example.com/users/1"
|> Flow.retry (
Schedule.exponential (TimeSpan.FromMilliseconds 100.0)
|> Schedule.jitteredWith (System.Random().NextDouble)
|> Schedule.recursAtMost 5
|> Schedule.whileInput notRateLimited)
jitteredUser: Flow<TestEnv,HttpError,User>Axial.Flow`3FsLiveDocsGeneratedPage2_03F7AABF2A94.TestEnvAxial.HttpClient.HttpErrorA recoverable HTTP transport, timeout, status, or decoding failure.
FsLiveDocsGeneratedPage2_03F7AABF2A94.UserAxial.HttpClient.HttpModulegetJson: (string -> Result<'value,string>) -> string -> Flow<'env,HttpError,'value>Sends a GET request and decodes the JSON response. <example><code>Http.getJson (Json.deserializeResult codec) "https://api.example.com/users/1"</code></example>
decodeUser: string -> Result<User,string>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowretry: Schedule<'env,'error,'output> -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Retries a flow's typed failures according to a schedule. The flow runs once, and after each Cause.Fail the schedule sees the error and decides whether to run it again and how long to wait. Defects and interruptions are never retried. When the schedule stops, the flow fails with the last error. Each attempt runs in its own child scope; a failed attempt's finalizers run before the next attempt starts. For the common case, build the schedule from a Retry record. Decides, from each typed error, whether to retry and after what delay. The flow to retry. A flow that succeeds as soon as an attempt succeeds. fetch |> Flow.retry (Schedule.recurs 3) fetch |> Flow.retry (Retry.schedule { Retry.defaults with When = HttpError.isTransient })
Axial.ScheduleModuleBuilds and combines schedules that decide whether, and after what delay, a flow runs again. Drive a schedule with Flow.retry (after typed failures), Flow.repeat (after successes), or FlowStream.fromSchedule (as a stream of its outputs). A schedule holds no state, so one value can drive any number of independent runs.
exponential: TimeSpan -> Schedule<'env,'input,TimeSpan>Creates a schedule that recurs with exponential backoff. The initial delay for the first retry. A schedule that recurs indefinitely, doubling the delay each time (baseDelay * 2^attempt) and capping at instead of overflowing. Thrown when is negative. let backoff () = Schedule.exponential (TimeSpan.FromMilliseconds 100.0) // Delays: 100ms, 200ms, 400ms, 800ms...
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
jitteredWith: (unit -> float) -> Schedule<'env,'input,'output> -> Schedule<'env,'input,'output>Adds jitter to a schedule's delay using a caller-supplied sample source. is not validated: a value outside [0.0, 1.0) is not rejected, it just produces a jitter factor outside the documented 0.5–1.5 range. The result is still always a valid, non-negative TimeSpan: negative factors clamp to and overflowing ones clamp to , the same as the base schedule's own overflow handling. This is deliberate: jitteredWith never throws for a badly-behaved sample source. A function returning a value in [0.0, 1.0), sampled once per attempt. Supply a deterministic function for reproducible schedules and tests. The base schedule to which jitter will be applied. A new schedule where each delay is multiplied by sample () + 0.5, giving a factor between 0.5 and 1.5, capped at . let schedule = Schedule.spaced (TimeSpan.FromSeconds 1.0) |> Schedule.jitteredWith (fun () -> 0.25) // Every delay becomes 750ms.
System``.ctor``: unit -> unitInitializes a new instance of the class, using a time-dependent default seed value.
NextDouble: unit -> floatReturns a random floating-point number that is greater than or equal to 0.0, and less than 1.0. A double-precision floating point number that is greater than or equal to 0.0, and less than 1.0.
recursAtMost: int -> Schedule<'env,'input,'output> -> Schedule<'env,'input,'output>Stops a schedule after at most recurrences, keeping its output and delays. Like recurs, counts recurrences after the first run, so recursAtMost 3 with Flow.retry allows 4 executions in total. The maximum number of recurrences. The schedule to cap. Schedule.exponential (TimeSpan.FromMilliseconds 100.0) |> Schedule.recursAtMost 5
whileInput: ('input -> bool) -> Schedule<'env,'input,'output> -> Schedule<'env,'input,'output>Continues only while the input satisfies a predicate. With Flow.retry the input is the typed error, so this selects which errors are worth retrying; an error that fails the predicate stops the schedule and the flow fails with it. With Flow.repeat the input is the successful value. Returns true for inputs that may recur. The schedule consulted for inputs that satisfy the predicate. Schedule.exponential (TimeSpan.FromMilliseconds 200.0) |> Schedule.recursAtMost 3 |> Schedule.whileInput HttpError.isTransient
notRateLimited: HttpError -> boolRequires & types
Requires: open Axial; open Axial.HttpClient; open System (packages: Axial, Axial.HttpClient)val jitteredUser: Flow<TestEnv,HttpError,User>A schedule without whileInput retries every typed error; select the transient ones so permanent failures stay
fast.
Long polls that restart on demand
When local state changes, a wake signal cancels the in-flight GET and starts a fresh one. Wakes do not count as retry failures; only an HTTP failure reaches the retry policy.
The endpoint below must support long polling, so this example needs a server and is not run by the docs build (no-check).
open Axial
open Axial.HttpClient
let pollUntilResponse (wakes: Queue<unit>) : Flow<TestEnv, HttpError, HttpResponse> =
let request = Http.get "https://api.example.com/events?wait=30"
let work = Http.send request |> Http.retryTransient 3 (TimeSpan.FromMilliseconds 200.0)
FlowStream.reissueOnWake (FlowStream.fromDequeue wakes) workCreate wakes with Queue.unbounded () inside a flow, and call Queue.offer () wakes when a local change should restart the poll.
Limit Response Size
A server you do not control can send a body of any size. Request.maxResponseBytes caps it:
let page (url: string) : Flow<TestEnv, HttpError, string> =
Http.get url
|> Request.maxResponseBytes (2L * 1024L * 1024L)
|> Http.text
page: string -> Flow<TestEnv,HttpError,string>url: stringstringAn abbreviation for the CLI type . Basic Types
Axial.Flow`3FsLiveDocsGeneratedPage2_03F7AABF2A94.TestEnvAxial.HttpClient.HttpErrorA recoverable HTTP transport, timeout, status, or decoding failure.
Axial.HttpClient.HttpModuleget: string -> HttpRequestCreates a GET request. <example><code>Http.get "https://api.example.com/users"</code></example>
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.HttpClient.RequestModulemaxResponseBytes: int64 -> HttpRequest -> HttpRequestLimits the response body to <paramref name="bytes" />. A larger body fails with <c>HttpError.ResponseTooLarge</c>: the live service checks <c>Content-Length</c> before reading and stops reading once the limit is passed, so an untrusted server cannot make the client buffer an unbounded body. <example><code>request |> Request.maxResponseBytes (1024L * 1024L)</code></example>
(*): ^T1 -> ^T2 -> ^T3Overloaded multiplication operator The first parameter. The second parameter. The result of the operation. 8 * 6 // Evaluates to 48
text: HttpRequest -> Flow<'env,HttpError,string>Sends a request and returns the response body text. <example><code>Http.get url |> Request.bearer token |> Http.text</code></example>
Requires & types
Requires: open Axial; open Axial.HttpClient (packages: Axial, Axial.HttpClient)val page: string -> Flow<TestEnv,HttpError,string>The live service fails with HttpError.ResponseTooLarge before reading when Content-Length declares a larger body,
and stops reading a streamed body once it passes the limit, so the client never buffers more than the limit.
Http.send applies the same check to responses from any IHttp, including test doubles. Request.tryMaxResponseBytes reads a
request's limit back, for example in a test double that enforces it.
Expected Statuses Are Part Of The Request
The expected status travels with the request, so callers cannot forget to check it:
let deleteUser (userId: int) : Flow<TestEnv, HttpError, HttpResponse> =
DELETE $"https://api.example.com/users/{userId}"
|> expect [ 204; 404 ] // idempotent delete: already-gone is fine
|> fetch
deleteUser: int -> Flow<TestEnv,HttpError,HttpResponse>userId: intintAn abbreviation for the CLI type . Basic Types
Axial.Flow`3FsLiveDocsGeneratedPage2_03F7AABF2A94.TestEnvAxial.HttpClient.HttpErrorA recoverable HTTP transport, timeout, status, or decoding failure.
Axial.HttpClient.HttpResponseThe complete response transcript for one HTTP exchange.
DELETE: FormattableString -> HttpRequestCreates a DELETE request from an interpolated URL with encoded holes.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
expect: int seq -> HttpRequest -> HttpRequestReplaces the statuses treated as success. <example><code>request |> expect [ 200; 404 ]</code></example>
fetch: HttpRequest -> Flow<'a,HttpError,HttpResponse>Sends the request and returns the full transcript, failing on unexpected statuses. <example><code>GET $"{root}/users" |> fetch</code></example>
Requires & types
Requires: open Axial; open Axial.HttpClient; open Axial.HttpClient.DSL (packages: Axial, Axial.HttpClient)val deleteUser: int -> Flow<TestEnv,HttpError,HttpResponse>deleteUser 7 |> Flow.run (ScriptedHttp([ 404, "" ]).Env) |> Exit.map _.StatusCode |> shouldEqual (Exit.Success 404)
deleteUser: int -> Flow<TestEnv,HttpError,HttpResponse>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
``.ctor``: (int * string) list -> ScriptedHttpEnv: TestEnvAxial.Exitmap: ('v -> 'w) -> Exit<'v,'e> -> Exit<'w,'e>Transforms the success value of an exit outcome using the provided function. The function to transform the success value. The exit outcome to transform. A new exit outcome with the transformed success value.
_arg1: HttpResponseStatusCode: intThe numeric response status code.
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Requires & types
Requires: open Axial; open Axial.HttpClient (packages: Axial, Axial.HttpClient)expectAny disables interpretation for endpoints where every status is meaningful, and Http.sendResult does the
same for one call without changing the request.
When Not To Retry
Do not wrap non-idempotent POSTs in retryTransient unless the server deduplicates requests (for example with an
idempotency key header): a timeout does not prove the server ignored the request. Send the key explicitly, then
retry safely:
type Payment = { Amount: decimal }
let encodePayment (payment: Payment) = $"{{\"amount\":{payment.Amount}}}"
let decodeReceipt (json: string) : Result<string, string> = Ok json
let pay (idempotencyKey: string) (payment: Payment) : Flow<TestEnv, HttpError, string> =
POST $"https://api.example.com/payments"
|> header "Idempotency-Key" idempotencyKey
|> jsonBodyOf encodePayment payment
|> fetchJson decodeReceipt
|> withRetries 3
FsLiveDocsGeneratedPage2_03F7AABF2A94.PaymentAmount: decimaldecimalAn abbreviation for the CLI type . Basic Types
encodePayment: Payment -> stringpayment: PaymentdecodeReceipt: string -> Result<string,string>json: stringstringAn abbreviation for the CLI type . Basic Types
Microsoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
pay: string -> Payment -> Flow<TestEnv,HttpError,string>idempotencyKey: stringAxial.Flow`3FsLiveDocsGeneratedPage2_03F7AABF2A94.TestEnvAxial.HttpClient.HttpErrorA recoverable HTTP transport, timeout, status, or decoding failure.
POST: FormattableString -> HttpRequestCreates a POST request from an interpolated URL with encoded holes. <example><code>POST $"https://api.example.com/users" |> jsonBody payload</code></example>
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
header: string -> string -> HttpRequest -> HttpRequestAppends one request header. <example><code>request |> header "Accept" "text/csv"</code></example>
jsonBodyOf: ('a -> string) -> 'a -> HttpRequest -> HttpRequestEncodes and sends a JSON body. <example><code>request |> jsonBodyOf (Json.serialize codec) user</code></example>
fetchJson: (string -> Result<'a,string>) -> HttpRequest -> Flow<'b,HttpError,'a>Sends the request and decodes the JSON response. <example><code>GET $"{root}/users/{id}" |> fetchJson (Json.deserializeResult codec)</code></example>
withRetries: int -> Flow<'a,HttpError,'b> -> Flow<'a,HttpError,'b>Retries transient failures with exponential backoff. <example><code>GET $"{root}/users" |> fetchJson decode |> withRetries 3</code></example>
Requires & types
Requires: open Axial; open Axial.HttpClient; open Axial.HttpClient.DSL (packages: Axial, Axial.HttpClient)val encodePayment: Payment -> stringval decodeReceipt: string -> Result<string,string>val pay: string -> Payment -> Flow<TestEnv,HttpError,string>Pass the key in from the caller: the request is built once, so the retry resends the same key and the server can recognise the repeat — and two separate invocations can share a key when the caller chooses one:
let payments = ScriptedHttp([ 503, ""; 200, "receipt-1" ])
pay "idem-checkout-1" { Amount = 9.5m } |> Flow.run payments.Env |> shouldEqual (Exit.Success "receipt-1")
payments.Sent
|> List.map (fun request -> Request.plan request |> _.Headers |> List.find (fst >> (=) "Idempotency-Key") |> snd)
|> List.distinct
|> List.length
|> shouldEqual 1
payments: ScriptedHttp``.ctor``: (int * string) list -> ScriptedHttppay: string -> Payment -> Flow<TestEnv,HttpError,string>Amount: decimal(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
Env: TestEnvshouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Sent: HttpRequest listMicrosoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ] This is an O(n) operation, where n is the length of the list.
request: HttpRequestAxial.HttpClient.RequestModuleplan: HttpRequest -> RequestPlanReturns a redacted request plan without sending anything. <example><code>Request.plan request</code></example>
_arg1: RequestPlanHeaders: (string * string) listfind: ('T -> bool) -> 'T list -> 'TReturns the first element for which the given function returns True. Raises KeyNotFoundException if no such element exists. The function to test the input elements. The input list. Thrown if the predicate evaluates to false for all the elements of the list. The first element that satisfies the predicate. let isEven x = 0 = x % 2 let isGreaterThan x y = y > x let input = [1, "Luke"; 2, "Kirk"; 3, "Spock"; 4, "Kenobi"] input |> List.find (fun (x,_) -> isEven x) // evaluates (2, "Kirk") input |> List.find (fun (x,_) -> x |> isGreaterThan 6) // raises an exception This is an O(n) operation in the worst case, where n is the length of the list.
fst: 'T1 * 'T2 -> 'T1Return the first element of a tuple, fst (a,b) = a. The input tuple. The first value. fst ("first", 2) // Evaluates to "first"
(>>): ('T1 -> 'T2) -> ('T2 -> 'T3) -> 'T1 -> 'T3Compose two functions, the function on the left being applied first The first function to apply. The second function to apply. The composition of the input functions. let addOne x = x + 1 let doubleIt x = x * 2 let addThenDouble = addOne >> doubleIt addThenDouble 3 // Evaluates to 8
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
snd: 'T1 * 'T2 -> 'T2Return the second element of a tuple, snd (a,b) = b. The input tuple. The second value. snd ("first", 2) // Evaluates to 2
distinct: 'T list -> 'T listReturns a list that contains no duplicate entries according to generic hash and equality comparisons on the entries. If an element occurs multiple times in the list then the later occurrences are discarded. The input list. The result list. let input = [6;1;2;3;1;4;5;5] input |> List.distinct Evaluates to [6; 1; 2; 3; 4; 5]. This is an O(n) operation, where n is the length of the list.
length: 'T list -> intReturns the length of the list. The input list. The length of the list. The notation list.Length is preferred. This is an O(n) operation, where n is the length of the list. let inputs = [ "a"; "b"; "c" ] inputs |> List.length Evaluates to 3 This is an O(n) operation, where n is the length of the list.
Requires & types
Requires: open Axial; open Axial.HttpClient (packages: Axial, Axial.HttpClient)val payments: ScriptedHttp
