v0.28.0 - Quality Release
This release contains a number of improvements to how bad input to Suave is handled. Previously we've done it the happy path. This has meant that bad input; especially so when it's input that's not parsed by a WebPart, but by the server, could cause a 'failwith' to throw an exception, cancelling the job (that serves the request) and leaving the Tcp connection hanging in an ever-connected state. By extension, this has meant an attacker could send many malformed requests that hang the TCP socket (requiring an equal number of sockets to be open from the attacker's machine(s)).
No more, we say, and with v0.28, Suave returns 400 Bad Request when it for example gets malforms HTML forms or requests without the Host-header.
More Improvements
We've improved how the WebSocket-code handles under different error cases, which means a more stable implementation.
SocketOp
There have been a number of breaking changes, mostly in the Suave.Utils
namespace and in Suave.Sockets
. One of the larger changes is that the socket
workflow builder is now brought into scope through open Suave.Sockets.Control
. The value that monad/workflow-builder operates on, SocketOp
has been recognised to be pretty versatile for error handling and control flow with asynchronous interleaved sockets with error handling (that's a mouthful!), so it's gotten its own module SocketOp
in Suave.Sockets
. A few of the utility functions around lifting async/task to socket have been moved here: { ofAsync
, ofTask
}.
The special part about this monad is that Suave uses its Error value in the Choice2Of2-case -- if its Case is SocketError then the connection is terminated without further ado, but if it's InputDataError Suave now takes care to ferry the error all the way to the HTTP response body to tell the caller about the error in its ways. You can do the same if you're using SocketOp, e.g. like exemplified in examples/WebSocket
, examples/Example/CounterDemo.fs
; so it's a really nice abstraction for writing safe code with.
This monad is structured very similar to Choice
, so you have functions like:
mreturn
- create a new successful valueabort
- create a new unsuccessful valueorInputError
- says that something is wrong with the input on a protocol level and that it's therefore a bad request (user input error) -- the error already present is overwritten with the errorMsg parameter.orInputErrorf
- same as the above, but let's you do something with the existing error message through the callback function passedbind
- Bind the result successful result of the SocketOp to fContbindError
- Bind the error result of the SocketOp to fContmap
- Map f over the contained successful value in SocketOpmapError
- Map f over the error value in SocketOpofAsync
- lift a Async<'a> type to the SocketOpofTask
- lift a Task type to the SocketOp
You can also open SocketOpOperators
in this same namespace, to gain access to:
(@|!)
- SocketOp.orInputError(@|!!)
- SocketOp.orInputErrorf(@|>)
- SocketOp.bindError
For example, here's how you write to a Server-Sent Event Stream and sleep async 100 ms before writing again:
let write i =
socket {
let msg = { id = i; data = string i; ``type`` = None }
do! msg |> send out
return! SocketOp.ofAsync (Async.Sleep 100)
}
More Changes
- There were a few straggling snake_case that were switched over to camelCase. If you get a compilation error for one of these, just try camelCase.
- The web server can now handle multipart/mixed content types that are nested in multipart/form when a form field has multiple values. This is a nice way to handle multiple values instead of using
value[]
keys and parsing in your own code - the main benefactor of this is form files. - You'll find a few utility methods return Choice1Of2 or Choice2Of2 instead of Option -- this is part of the major refactor towards providing contextual error messages everywhere and failing gracefully.