Skip to content

0.5.0

Choose a tag to compare

@zaiev zaiev released this 14 Oct 14:24
· 58 commits to main since this release

Spindle moves to wiretype 0.5.0 and rowtype 0.7.0, renames its packages, types more of HTTP, keeps secrets out of logs, and fixes compressed answers and proxied clients.

Upgrading

Packages and dependencies

  • The packages are spindle-postgres and spindle-cli, as are their libraries: (libraries spindle_postgres) → (libraries spindle-postgres). Module names are unchanged.
  • The log source spindle_postgres → spindle-postgres.
  • spindle needs wiretype 0.5.0 and Uuidm; spindle-postgres needs rowtype 0.7.0 and postgres-eio 0.5.0.

Codecs

  • Codec.uuid is a Uuidm.t, was a string, so one id is one type in a body and in a row.
  • Codec.custom ~kind ~parse ~print (): ~parse answers Ok v or Error "what the client is told", where it answered an option with ?expects.
  • Codec.parse answers a result.

Requests and routes

  • Spindle.body_stream ~content_type:(fun ct -> …) → ~content_types:[ "application/octet-stream" ], each type/subtype, type/* or type/*+suffix.
  • Dep.of_body ~content_type is gone: the types go in its need, Dep.Raw { content_types }.
  • A route's content type that is not a valid pattern ("json") makes App.make raise; Compress.make ~types raises on one too.
  • ?or_not_found:() → ?unparsed:Next_route on a path parameter; Route.param's field or_not_found → unparsed.
  • Cors.make ~headers → ~allow_headers.
  • ?now / ~now on Spindle.serve, Server.run, Server.serve_on and Request.make → read_clock. Test.call ~now and Session's ~now stay a Ptime.t.
  • Spindle.clock_now clock () → Spindle.of_clock clock.

HTTP types

  • Status.t names every RFC 9110 status plus 103, 425, 428, 451 and 511: `Code 402 → `Payment_required.
  • Meth.t has `CONNECT and `TRACE, where they were `Other s.
  • Websocket.send answers Error (Unwritable _) for text that is not UTF-8 or JSON it cannot encode, and sends nothing; a handler returning it closes with 1011.
  • Spindle_http.Multipart.to_string answers a result, refusing a bad boundary or content type, or content holding the delimiter.

Postgres

  • Spindle_postgres.connect and Pool.create take a Conninfo.t: "postgres://…" → Rowtype_postgres.Conninfo.of_string "postgres://…". Pool.run still takes the string.
  • ?connect_timeout is gone: Conninfo.with_connect_timeout. Without one it is still 10 s.
  • Pool.query → Pool.exec.
  • Spindle_postgres.error is [ Rowtype.error | Rowtype_postgres.Transaction.failure | Rowtype_postgres.Pool.error ]; its printer is Rowtype_postgres.error_to_string.

Client

  • Spindle_client.error is [ `Unsent of string | `Lost of string ]: Unanswered and Timeout → `Lost.
  • websocket_error is [ error | `Not_upgraded of response | `Websocket of Spindle_http.Websocket.error ], where it was Failed, Refused and Ended.
  • events_end's Refused → Answered.

Logging

  • An exception's message is never logged: Log.raised writes error.kind and error.stack, no error.message, and a raise's 500 names the exception's kind.

OpenAPI and Zod

  • Routes told apart only by a parameter's type are one operation, its parameter any of their types.
  • Paths that differ only in a parameter's name (/items/{id}, /items/{item}) are the document's Error: give them one name.
  • Routes of one operation that answer differently or read different bodies are an Error too.

New

  • Codec.enum's ~kind is optional.
  • error_to_string beside every public error: Session, Body, Multipart, Spindle_http.Head, Head.Response, Spindle_http.Multipart; Test.websocket_error_to_string.
  • Spindle_http.Media_type.is_pattern and Media_type.matches check and compare content types.
  • A raw body's content types are listed in the OpenAPI document, where it always said application/octet-stream.
  • A database that cannot be reached (`Unsent) answers 503 busy with Retry-After: 1, logged as a warning.
  • An answer outside its description's bounds is never written: it is a logged 500. Read-only members are not read, write-only ones never answered.

Fixed

  • A client behind a trusted proxy whose X-Forwarded-For has a port (1.2.3.4:5678) is that address, so Rate.limit by client limits again.
  • A compressed answer revalidates: If-None-Match with its -gzip tag answers 304, and If-Match with it passes.
  • Every 304 says Vary: Accept-Encoding when its 200 would, Static and Files included.
  • A client call's timeout covers the DNS lookup; a slow resolver held it for as long as it took.
  • A log line is always valid UTF-8 on one line; Pretty writes line breaks in a message as \n and \r.
  • A body read a handler left running is stopped when the handler returns, where the connection hung.
  • A 426 sends Connection: upgrade with its Upgrade.
  • A client that left before 100 Continue makes the body read answer Error (Unreadable _), not raise.
  • A stream's send after its producer has returned answers Error Gone.
  • A WebSocket takeover in flight when the server stops gets the drain to close, not cancelled at once.