All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
- Static file responses no longer serve XML-based documents inline, which allowed stored XSS
when an application served attacker-supplied uploads.
image/svg+xmlandtext/xmlwere classified asinlineby their top-levelimage/texttype, so an uploaded SVG carrying<script>— or an XML document naming an XSLT stylesheet through<?xml-stylesheet ?>— executed script in the serving origin when opened. XML-based content types now default toattachment, matching howapplication/xmlandapplication/xhtml+xmlalready behaved. That covers the+xmlsuffix, anxmlsubtype, RFC 7303'sxml-dtdandxml-external-parsed-entity, and the legacytext/xslthat<?xml-stylesheet ?>itself names.text/htmlstill defaults toinline. Reported by sl91994. NamedFileandStaticEmbedresponses now carryX-Content-Type-Options: nosniff.salvo-otelno longer records the full request URI.Metricsdroppedurl.fullentirely, so query strings no longer become metric dimensions.Tracingreplaced it withurl.pathplus aurl.querywhosesig,Signature,AWSAccessKeyIdandX-Goog-Signaturevalues are redacted, as the semantic conventions require.
NamedFileBuilder::use_content_type_optionsandNamedFile::use_content_type_optionsto control theX-Content-Type-Optionsheader.StaticDir::disposition_type,StaticDir::use_content_type_options,StaticFile::disposition_type,StaticFile::attached_nameandStaticFile::use_content_type_options, so applications serving trusted assets can opt back into inline rendering, which previously had no public API on either handler.NamedFileBuilder::disposition_name, which sets the nameContent-Dispositionreports without forcing the disposition toattachmentthe wayattached_namedoes.- Cookie extractors now support structured JSON values.
CookieParam<T>falls back to JSON when scalar conversion fails, while#[derive(Extractible)]accepts explicit cookie field sources such as#[salvo(extract(source(from = "cookie", parse = "json")))]. - Changelog established for upcoming releases.
- Opt-in RFC 9457 Problem Details responses with typed extension members and OpenAPI integration
via the
rfc9457feature. - Initial opt-in OpenAPI 3.2 document support in
salvo-oapi, including the$selffield. - OpenAPI 3.2 object model in
salvo-oapi:- Tag Object
summary,parentandkind. - Server Object
name. - Path Item Object
query(viaPathItemType::Query) andadditionalOperations. - Parameter Object
in: querystring(ParameterIn::QueryString) andstyle: cookie(ParameterStyle::Cookie). - Media Type Object
itemSchema,prefixEncodinganditemEncoding. - Encoding Object nested
encoding,prefixEncodinganditemEncoding. - Example Object
dataValueandserializedValue. - Response Object
summary, anddescriptionis now optional on input. - Components Object
mediaTypes. - Discriminator Object
defaultMapping. - XML Object
nodeType(newXmlNodeTypeenum). - Security Scheme Object
deprecatedandoauth2MetadataUrl. - OAuth Flows Object
deviceAuthorization(newDeviceAuthorizationflow). - Info Object
summary.
- Tag Object
- Header Object gained the rest of its spec surface:
required,deprecated,style,explode,example,examples,contentand extensions, plusHeader::with_content. - Route discovery emits
QUERYoperations and custom HTTP methods (viaadditionalOperations) when the document declares OpenAPI 3.2. Documents that still declare 3.1 skip such routes and log a warning explaining how to opt in. #[endpoint]parameters accept theQueryStringlocation and theCookiestyle.Content::from_ref/Content::ref_location, so acontentmap entry can reference a reusable Media Type Object without changing those maps toRefOr<Content>.examples/oapi-3-2demonstrates emitting a 3.2 document with aQUERYroute.salvo_core::fs::extension_content_encoding, which reports the content coding a file extension implies, so a handler choosing a file to serve can tell that it already carries one.salvo_core::conn::rustls::default_crypto_provider, which reports the rustlsCryptoProviderSalvo builds its TLS configurations with. Pass it to other rustls based libraries so the whole application agrees on one backend.salvo-otelrecordshttp.server.active_requests,http.server.request.body.sizeandhttp.server.response.body.size, which its documentation already described. The in-flight gauge is decremented from a drop guard, so a cancelled request — a client disconnect or a timeout — does not leave it drifting upwards.Metrics::with_known_methodsandTracing::with_known_methodswiden the set of request methods reported verbatim inhttp.request.method, for applications serving methods beyond RFC 9110 and RFC 5789 (WebDAV'sPROPFIND, say).salvo-otelgained amatched-pathfeature, on by default, which supplieshttp.route. Through thesalvocrate it follows that crate's ownmatched-pathfeature.
- Breaking. Several public enums and structs gained variants or fields, which can break
downstream exhaustive matches and struct literals even though the wire format change is
purely additive:
PathItemType::Query,ParameterIn::QueryString,ParameterStyle::CookieandFlow::DeviceAuthorizationare new variants.SecurityScheme::MutualTlsgained adeprecatedfield.Header::schemais nowOption<RefOr<Schema>>so a header can describe its value withcontentinstead.Header::default()still yields aStringschema, so serialized output is unchanged.
OpenApistill defaults to emitting OpenAPI 3.1; 3.2 remains opt-in viaOpenApi::openapi_version.- Breaking.
salvo-otel'sMetricsmiddleware now emits the instruments the OpenTelemetry HTTP semantic conventions define, replacing names it had invented. Dashboards and alerts built on the old names have to be repointed:salvo_request_duration_msbecamehttp.server.request.duration, and it measures seconds rather than milliseconds, with the bucket boundaries the conventions recommend. A Prometheus exporter renders it ashttp_server_request_duration_seconds.salvo_request_countandsalvo_error_countare gone. The conventions define no such instruments: the duration histogram already carries the request count (http_server_request_duration_seconds_count), and failures are selected by filtering onerror.typeorhttp.response.status_code.- The instruments are registered under the
salvo-otelinstrumentation scope, versioned and carrying the semantic conventions' schema URL, instead of a baresalvometer.
- Breaking.
salvo-otel's metric dimensions changed to the ones the conventions list, so the number of time series is now proportional to the number of routes instead of growing with traffic:url.full— one series per distinct URI, query string included — was replaced byhttp.route, the matched route template (/users/{id}). A request that matched no route carries nohttp.routeat all rather than a fallback, and a goal mounted at the router root is reported as/.exception.messagewas replaced byerror.type, which carries the status code of a server error. The old attribute was unbounded, and it was appended to the labels ofsalvo_request_countandsalvo_request_duration_msbut not ofsalvo_error_count, so a failed request produced a different label set than a successful one on the same metric.- A request method outside RFC 9110 and RFC 5789 is now reported as
_OTHER, so a client cannot open a time series per made-up method name. SeeMetrics::with_known_methods. url.schemeandnetwork.protocol.versionwere added. A request target given in absolute form lets the client choose the scheme, so a scheme other thanhttporhttpsis reported as_OTHERfor the same reason an unknown method is; spans still record it.
- Breaking.
salvo-otel'sTracingmiddleware follows the same conventions:- Spans are named
{method} {route}—GET /users/{id}— instead of{method} {uri}, which made every distinct URI its own span name. url.fullgave way tourl.path, a redactedurl.queryandurl.scheme, andhttp.routewas added.network.protocol.versionreports1.1and2rather than theHTTP/1.1formVersion'sDebugoutput produces.- An unknown request method is reported as
_OTHER, with the value the client sent kept inhttp.request.method_original. - A
5xxresponse now setserror.typeand an error span status. A4xxdoes not: it is a valid outcome for a server span. http.response.header.content-length, which was not an attribute the conventions define, becamehttp.response.body.size, and it is left out for a response whose body the catcher fills in after middleware returns rather than reported as zero.client.addressreports the peer's IP (198.51.100.4) with the port split out intoclient.port, instead of salvo'ssocket://198.51.100.4:54321display form.- The
telemetry.sdk.name,telemetry.sdk.versionandtelemetry.sdk.languageattributes were removed from every span. They describe the resource, and the SDK already reports them there.
- Spans are named
- Behavior change. Serving an SVG or XML file through
StaticDir,StaticFileorNamedFilenow sendsContent-Disposition: attachmentby default, so following a link to one downloads it instead of rendering it, and<object>/<embed>/<iframe>no longer display it. Referencing the same file from<img>, CSSurl()or<use>is unaffected, because browsers ignoreContent-Dispositionon subresource loads. Directories holding only trusted assets can restore the old behavior withStaticDir::new(..).disposition_type("inline").text/htmlstill defaults toinline, since serving HTML documents is the point of a static file server.
- Documentation UI compatibility with a 3.2 document, smoke-tested against the bundled Swagger
UI v5.32.11 and the current CDN builds of the others: all four load the document, but only
Swagger UI renders
queryoperations. Scalar, RapiDoc and ReDoc display the rest of the document and silently omit them. Their CDN URLs are deliberately left unpinned, since no released build of those three rendersqueryyet.
- TLS setup no longer panics with "Could not automatically determine the process-level
CryptoProviderfrom Rustls crate features". rustls can only pick a backend by itself when exactly one of itsaws-lc-rsandringfeatures is enabled across the whole dependency graph, so pulling in any crate that enables the other one madeRustlsListener,QuinnListener,AcmeListenerand the proxy's defaultHyperClientpanic. Salvo now selects the provider from its own features and passes it to rustls explicitly. An application that installed a process level provider throughCryptoProvider::install_defaultstill has that one used; Salvo itself never installs one, so applications keep full control over the process level default. StaticDirnames a download after the file that was requested rather than the precompressed sidecar it was served from, so a request forlogo.svganswered out oflogo.svg.brno longer offersContent-Disposition: attachment; filename="logo.svg.br".PathItemno longer duplicates operations intoextensionswhen deserialized, which previously made a parsed path item re-serialize with repeated keys.- OAuth2 flows are now resolved by their flow name instead of by shape, so a
clientCredentialsflow no longer deserializes asFlow::Password. - Objects whose optional fields are skipped during serialization can now be deserialized
again; previously round-tripping a generated document failed with
missing fielderrors. .svgzfiles are now served withContent-Encoding: gzip. Their extension names both a media type and the coding applied to it, but an extension resolves to a single media type, so the response described the gzip stream asimage/svg+xmland no client could render it. The gzipped X3D forms.x3dz,.x3dvzand.x3dbzare handled the same way..gzand.tgzare unaffected, since there the gzip stream is the representation being served.StaticDiralso stops serving a precompressed sidecar for such a file — alogo.svgz.brstacks a second coding on the gzip, and only the outer one can be reported.
Releases published before this changelog was introduced are available in the GitHub Releases page: https://github.com/salvo-rs/salvo/releases