Download Latest Version v3.7.2 source code.zip (2.0 MB) Google Add to Preferred Sources
Home / v3.7.0
Name Modified Size InfoDownloads / Week
Parent folder
Suave.3.7.0.nupkg 2026-10-15 850.7 kB
Suave.3.7.0.symbols.nupkg 2026-10-15 1.1 MB
Suave.DotLiquid.3.7.0.nupkg 2026-10-15 25.5 kB
Suave.DotLiquid.3.7.0.symbols.nupkg 2026-10-15 28.8 kB
Suave.Json.3.7.0.nupkg 2026-10-15 16.1 kB
Suave.Json.3.7.0.symbols.nupkg 2026-10-15 17.3 kB
README.md 2026-10-15 14.2 kB
v3.7.0 source code.tar.gz 2026-10-15 1.8 MB
v3.7.0 source code.zip 2026-10-15 2.0 MB
Totals: 9 Items   5.8 MB 0
  • 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
Source: README.md, updated 2026-10-15