Suave 3.7.0
See the version list below for details.
dotnet add package Suave --version 3.7.0
NuGet\Install-Package Suave -Version 3.7.0
<PackageReference Include="Suave" Version="3.7.0" />
<PackageVersion Include="Suave" Version="3.7.0" />
<PackageReference Include="Suave" />
paket add Suave --version 3.7.0
#r "nuget: Suave, 3.7.0"
#:package Suave@3.7.0
#addin nuget:?package=Suave&version=3.7.0
#tool nuget:?package=Suave&version=3.7.0
Suave is a simple web development F# library providing a lightweight web server and a set of combinators to manipulate route flow and task composition.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- FSharp.Core (>= 10.0.102)
NuGet packages (64)
Showing the top 5 NuGet packages that depend on Suave:
| Package | Downloads |
|---|---|
|
Aardvark.Service
Aardvark is an open-source platform for visual computing, real-time graphics and visualization. |
|
|
FsLab
FsLab is a combination package that supports doing data science with F#. FsLab includes literate scripting converted to HTML and PDF, and by default references Deedle (a data frame library), FSharp.Data (for data access) and XPlot (for visualization). You can optionally add any other nuget packages. |
|
|
Suave.DotLiquid
Suave.DotLiquid provides Liquid templates for Suave web server. |
|
|
SuaveServerWrapper
A wrapper for suave.io APIs to call from C# |
|
|
Logary.Services.SuaveReporter
Logary is a high performance, multi-target logging, metric and health-check library for mono and .Net. |
GitHub repositories (2)
Showing the top 2 popular GitHub repositories that depend on Suave:
| Repository | Stars |
|---|---|
|
msarilar/EDEngineer
An overlay to track Elite Dangerous blueprints progress in real time
|
|
|
rogeralsing/Microphone
|
| Version | Downloads | Last Updated |
|---|---|---|
| 3.7.2 | 59 | 10/15/2026 |
| 3.7.1 | 42 | 10/15/2026 |
| 3.7.0 | 41 | 10/15/2026 |
| 3.6.1 | 604 | 10/7/2026 |
| 3.6.0 | 148 | 10/5/2026 |
| 3.5.0 | 831 | 9/9/2026 |
| 3.4.6 | 6,489 | 8/29/2026 |
| 3.4.5 | 192 | 8/26/2026 |
| 3.4.4 | 3,448 | 7/13/2026 |
| 3.4.3 | 10,903 | 6/1/2026 |
| 3.4.2 | 586 | 5/31/2026 |
| 3.4.1 | 279 | 5/30/2026 |
| 3.4.0 | 512 | 5/18/2026 |
| 3.4.0-beta | 148 | 5/14/2026 |
| 3.3.1 | 398 | 5/11/2026 |
| 3.3.1-beta3 | 143 | 5/6/2026 |
| 3.3.1-beta2 | 137 | 5/5/2026 |
| 3.3.1-beta1 | 129 | 5/5/2026 |
| 3.3.1-beta | 139 | 5/4/2026 |
| 3.3.0 | 372 | 5/2/2026 |
Fix HTTP/2 responses whose content is a `SocketTask` being sent with status 200 and an empty body: `Files.file`, `Files.browse`, `Stream.okStream`, `Stream.okStreamChunked`, `TransferEncoding.chunked`, `EventSource.handShake` and custom writers now work over HTTP/2 (TLS with ALPN `h2`, h2c upgrade and h2c prior knowledge). Since HTTPS bindings advertise `h2` first, browsers received empty static files and event streams from 3.6.1 and earlier. The HTTP/2 writer gives the task a per-stream connection: header lines the task writes join the HEADERS frame, HTTP/1.1 chunk framing is removed, and the body goes out as flow-controlled DATA frames as it is written; once the client resets the stream, writes return an error
HTTP/2 responses no longer carry connection-specific headers (`Connection`, `Keep-Alive`, `Proxy-Connection`, `Transfer-Encoding`, `Upgrade`, RFC 9113 §8.2.2); `Transfer-Encoding: chunked` made clients fail the stream with PROTOCOL_ERROR (curl exit 92)
New `SuaveConfig.http2Enabled` (default `true`, also `withHttp2Enabled` and `HttpRuntime.http2Enabled`): when `false`, HTTPS bindings offer only `http/1.1` via ALPN, `Upgrade: h2c` is ignored and the HTTP/2 prior-knowledge preface is refused with 400. Code that builds `SuaveConfig` or `HttpRuntime` with a full record expression must add the field; `{ defaultConfig with ... }` is unaffected
The h2c `101 Switching Protocols` response, and any other 1xx response, no longer carries the default `Content-Type: text/html`
HTTP/2 response header blocks are HPACK-encoded under the connection's write lock, so concurrent streams cannot write their HEADERS frames in a different order from the encoder's dynamic table updates
Behaviour change:** `defaultErrorHandler` no longer returns the exception message and stack trace to "local" clients; every client gets a plain `500 Internal Server Error`. The local check used the connection address, or an `X-Forwarded-For`/`X-Real-IP` header the client could set to `127.0.0.1`; behind a reverse proxy on the same host (nginx -> 127.0.0.1) every remote client received stack traces. For development, opt in with the new `developmentErrorHandler` (`{ defaultConfig with errorHandler = developmentErrorHandler }`), which shows the HTML-encoded details to every client
HTTP/2 requests now carry the binding they arrived on in `request.binding`; it was the default `http://0.0.0.0:8080/`, so `request.binding.scheme.secure` was `false` and `request.url` was wrong for HTTPS clients that negotiated HTTP/2 (a redirect-to-HTTPS check looped for them)
Docs: the deployment guide uses .NET 10 and the `runtime` image with a non-root user and a curl-free health check, shows graceful shutdown on SIGTERM, fixes the nginx WebSocket/keep-alive setup, uses `X509CertificateLoader`, and replaces the ASP.NET Core Module IIS example with HttpPlatformHandler; the HTTPS recipe's redirect keeps the query string and port
`Validation.String` validators (`minLength`, `maxLength`, `exactLength`, `lengthBetween`, `matches`, `email`, `alphanumeric`, `alpha`, `numeric`, `contains`, `startsWith`, `endsWith`) return `Invalid` with "Value is required" for a `null` input instead of throwing `NullReferenceException`, e.g. for a field System.Text.Json left `null`; `notEmpty` is unchanged
`ValidationWebParts.validationErrorsToJson` writes its output with `Utf8JsonWriter`, so fields and messages containing `"`, `\` or control characters produce valid JSON; the shape is unchanged, the output is now compact (no spaces), and characters such as `'`, `<` and non-ASCII are written as `\uXXXX` escapes
Breaking: the `<++>` operator now preserves left-to-right order: `a <++> b` reports `a`'s errors before `b`'s and returns `a`'s value when both pass (previously the reverse). `a |> andAlso b` is unchanged
`HttpRequest.query` returns `(key, None)` for a query key without a value (`?flag`), as documented, instead of `(key, Some "")`; `?key=` is still `(key, Some "")`. `queryFlag "flag"` is now `true` for `?flag`, and `queryParam "flag"` returns `Choice2Of2` for it. Form parsing (`form`, `formData`) is unchanged
`pathScan`, `pathScanCi` and `Sscanf.sscanf` no longer wrap integers that are out of range for the target type: `/items/99999999999` against `pathScan "/items/%d"` with an `int` handler now declines instead of passing `1215752191`; the same applies to `int16`, `uint16` and `uint32` (and `%x`/`%X`/`%o` values that do not fit)
Behaviour change:** `CORS.defaultCORSConfig` no longer allows credentials (`allowCookies = false`). It used to reflect any `Origin` together with `Access-Control-Allow-Credentials: true`, which lets any web site make authenticated requests as your users. To allow credentials, list the trusted origins too: `{ defaultCORSConfig with allowedUris = InclusiveOption.Some [ "https://app.example.com/" ]; allowCookies = true }`
`cors` adds `Vary: Origin` (merged into an existing `Vary`) so caches do not serve one origin's CORS response to another
`cors` no longer sends `Access-Control-Allow-Methods: GET,` for a single allowed method; methods are joined as `GET, POST`
Add `CORS.withCORS config handler`, which answers preflight requests with 204 itself and runs `handler` only for actual requests, and `CORS.corsPreflight config`, which answers preflights and declines other requests (`choose [ corsPreflight cfg; cors cfg >=> app ]`). `cors cfg >=> handler` still lets the handler replace the preflight's 204, as documented
Behaviour change:** `RateLimit.defaultKeyExtractor` keys by the connection's remote address and ignores `X-Forwarded-For`/`X-Real-IP`, which a client could change on every request to avoid the limit. Behind a reverse proxy use the new `RateLimit.trustedProxyKeyExtractor [ proxyAddress ]`, which reads `X-Forwarded-For` only on connections from the listed proxies. `userKeyExtractor` and `headerKeyExtractor` fall back to the new default
Each `rateLimit` WebPart keeps its own counters. All limiters used to share one store keyed only by the extracted key, so two limiters keyed by IP counted together, and limiters with different strategies denied every request after the first (e.g. a `FixedWindow` limiter returned 429 after a `SlidingWindow` one had seen the same IP). Idle entries are still cleaned up. `getStats` returns the first limiter that has an entry for the key
Add `RateLimit.defaultRateLimitConfig`; `RateLimit.defaultConfig` is obsolete because after `open Suave.RateLimit` it shadows the server's `defaultConfig`. The obsolete alias still shadows it until it is removed; use `Web.defaultConfig` meanwhile
`securityHeaders` replaces headers of the same name already on the response instead of adding a second copy (e.g. two `Cache-Control` headers)
Security header presets send `X-XSS-Protection: 0` instead of `1; mode=block`. `strictConfig` no longer sends the deprecated `block-all-mixed-content` or HSTS `preload` (opt in with `withHSTS 31536000 true true`), and sends `Cache-Control: no-store` instead of `no-store, no-cache`
`Files.sendFile` (and so `file`, `browse`, `browseHome`, `browseFile`) treats `Range` byte positions as inclusive: `bytes=2-5` is now answered with `Content-Range: bytes 2-5/10` and four bytes instead of three. Open-ended (`bytes=5-`) and suffix (`bytes=-3`) ranges are supported, a range past the end is clamped, a range starting at or after the end is answered with `416` and `Content-Range: bytes */<length>`, and malformed, multi-range or non-`bytes` headers get the whole file. Partial responses are never compressed
`Files.sendFile` no longer sends `Content-Range` on full `200` responses, compressed or not; it is only sent with `206` and `416`
Upload temp files written for `request.files` are deleted once the response has been written (or the connection ends) instead of being left in the temp folder. Move or copy an upload during the request to keep it; streaming it back with `Files.sendFile` still works. Files written by a custom `filePartSink` are not touched
`Files.browse`, `browseHome`, `browseFile`, `browseFileHome` and `dir` decline a request whose path escapes the root (such as `/../secret.txt`) instead of throwing, so it gets the application's 404 rather than a 500. New `Files.tryResolvePath`; `resolvePath` still throws
When the compressed-files folder cannot be written to (the default, `AppContext.BaseDirectory`, is read-only under `dotnet fsi` and in read-only containers), compressible files are served uncompressed instead of the connection being closed without a response; the failure is reported once per folder through `Globals.writeMessage`
Docs: the file upload recipe and the file handling guide no longer build the destination path from the client-supplied file name, which allowed writing outside the upload folder with `filename=../../x`
The Suave.DotLiquid package declares `DotLiquid >= 2.3.197`; 3.6.1 and earlier declared `DotLiquid` with no lower bound (`0.0`), so NuGet picked DotLiquid 0.0.0 and the first render failed with `MissingMethodException` (`Template.set_NamingConvention`). Workaround for those versions: reference `DotLiquid` 2.3.197 explicitly. The DotLiquid docs now use `| escape` in their examples (DotLiquid does not HTML-escape output) and cover the response charset and template field naming
`open Suave` no longer shadows F#'s `Result.Ok`/`Result.Error`: the parser's `ScanResult` type (`NeedMore | Found | Error`) leaked its `Error` case into the `Suave` namespace, so bare `Error "x"` or `| Error e ->` in user code resolved to it. `ScanResult` is now internal and requires qualified access, and `HttpReader.scanMarker` and `HttpReader.skip`, which returned it, are now internal too. Code that matched on `ScanResult` or called those members must use `HttpReader.readUntilPattern` instead
Fix multipart/form-data bodies whose `multipart/mixed` part is followed by another part: the parser did not wait for the rest of the outer boundary line to be skipped, so when that data had not arrived yet the request hung or lost the parts after it
Security: `EventSource.data` and `EventSource.send` write one `data:` line per line of the data (split on CRLF, LF and CR), so line breaks in a message can no longer end the event and inject fields or extra events (e.g. `Message.create "1" "hello\n\nevent: admin\ndata: x"` produced a second `admin` event). `esId`, `eventType`, `comment` and the message `id`/`type` have CR and LF removed. `send` now writes the whole message in one write
Write failures are no longer swallowed, so streaming loops stop when the client disconnects (an `EventSource.send` loop used to keep running forever): `Connection.asyncWriteBytes`, `Connection.writeChunk`, `EventSource.send`/`data`/`comment`/`dispatch`/…, `transferStream` and `transferStreamChunked` raise the new `Suave.Sockets.SocketWriteException` (an `IOException` carrying the socket `Error`) when a write fails, which ends the response and closes the connection. New non-throwing variants return `Task<Result<unit, Error>>`: `Connection.tryWriteBytes`, `Connection.tryFlush`, `Connection.tryWriteChunk`, `Connection.tryWriteLastChunk`, `EventSource.trySend` and `EventSource.tryComment`. `TcpTransport`/`SslTransport.write` return `Error` instead of throwing when the socket is already shut down
`Connection.writeChunk` writes the raw bytes: it converted them to a UTF-8 string first, so non-UTF-8 data was corrupted and no longer matched the declared chunk length (`FF FE 00` was declared as 3 bytes and sent as 7). New `Connection.writeLastChunk` / `tryWriteLastChunk` write the terminating `0\r\n\r\n` chunk (as `writeChunk [||]` does)
`TransferEncoding.chunked` answers 200 when the response still has the initial 404 status of a fresh response (`TransferEncoding.chunked writer` alone used to answer 404); any other status set before it is kept, and a chunked 404 can be sent with `TransferEncoding.chunked writer >=> Writers.setStatus HTTP_404`. The response headers are now flushed before the writer runs, so writers can write straight to `conn.transport`
Breaking: `EventSource.handShake` keeps the headers already set on the response (it replaced them all) and no longer sends `Access-Control-Allow-Origin: *`. To allow cross-origin `EventSource` clients, compose it with `Writers.setHeader "Access-Control-Allow-Origin" "*"` (or your origin) or `CORS.cors`. `Content-Type` is always `text/event-stream; charset=utf-8`; `Cache-Control: no-cache` and `X-Accel-Buffering: no` are added unless already set
WebSocket frames are limited to `SuaveConfig.webSocketMaxFrameSize` bytes (new field, default 16 MiB; also on `HttpRuntime`). A frame announcing a larger payload is answered with a Close frame carrying code 1009 and the connection is dropped before any buffer is allocated or the `readIntoByteSegment` length function is called; previously a client could announce up to 2 GB per frame and `read ()` allocated it up front. Override per socket with `WebSocket.maxFrameSize`. Code that builds `SuaveConfig` or `HttpRuntime` without `{ defaultConfig with ... }` / `{ HttpRuntime.empty with ... }` must set the new field
An exception thrown by a WebSocket handler (`handShake`, `handShakeTask`, `handShakeWithSubprotocol`) is reported through `Globals.writeMessage` and answered with a Close frame carrying code 1011 (new `CloseCode.CLOSE_INTERNAL_ERROR`); previously it escaped and Suave wrote an `HTTP/1.1 500` response, with the exception text for local clients, onto the upgraded connection
`handShake` no longer writes an `HTTP/1.1 404 Not Found` response onto the connection when the WebSocket handler finishes, and a refused handshake now gets its `400 Bad Request` / `405 Method Not Allowed` instead of `404`
Close frames sent by Suave carry a two-byte status code as RFC 6455 requires (they sent four bytes, so clients read status 0)
`101 Switching Protocols` (and other 1xx) responses no longer get a default `Content-Type: text/html` header
WebSocket docs and recipes check the `Origin` header before `handShake` (cross-site WebSocket hijacking), answer Ping with Pong and Close with Close, and the broadcast recipe uses a `MailboxProcessor` hub that drops clients whose send fails