diff --git a/src/Giraffe/Core.fs b/src/Giraffe/Core.fs index 06e6413f..2e66e9f6 100644 --- a/src/Giraffe/Core.fs +++ b/src/Giraffe/Core.fs @@ -7,6 +7,7 @@ module Core = open System.Globalization open Microsoft.AspNetCore.Http open Microsoft.Extensions.Logging + open Microsoft.Net.Http.Headers open Giraffe.ViewEngine /// @@ -227,11 +228,13 @@ module Core = /// /// A Giraffe function which can be composed into a bigger web application. let mustAccept (mimeTypes : string list) : HttpHandler = + let acceptedMimeTypes : MediaTypeHeaderValue list = mimeTypes |> List.map (MediaTypeHeaderValue.Parse) fun (next : HttpFunc) (ctx : HttpContext) -> let headers = ctx.Request.GetTypedHeaders() headers.Accept - |> Seq.map (fun h -> h.ToString()) - |> Seq.exists (fun h -> mimeTypes |> Seq.contains h) + |> Seq.exists (fun h -> + acceptedMimeTypes + |> List.exists (fun amt -> amt.IsSubsetOf(h))) |> function | true -> next ctx | false -> skipPipeline diff --git a/tests/Giraffe.Tests/HttpHandlerTests.fs b/tests/Giraffe.Tests/HttpHandlerTests.fs index 054398d6..4a5d5695 100644 --- a/tests/Giraffe.Tests/HttpHandlerTests.fs +++ b/tests/Giraffe.Tests/HttpHandlerTests.fs @@ -381,6 +381,68 @@ let ``POST "/either" with unsupported Accept header returns 404 "Not found"`` () Assert.Equal(404, ctx.Response.StatusCode) } +[] +let ``POST with "all-medias" header type returns the first available route`` () = + /// Reference: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + let ctx = Substitute.For() + let app = + choose [ + POST >=> choose [ + route "/any" >=> mustAccept [ "text/plain" ] >=> text "first route" + route "/any" >=> mustAccept [ "application/json" ] >=> json "second route" + route "/any" >=> mustAccept [ "text/plain"; "application/json" ] >=> text "third route" ] + setStatusCode 404 >=> text "Not found" ] + + let headers = HeaderDictionary() + headers.Add("Accept", StringValues("*/*")) + ctx.Request.Method.ReturnsForAnyArgs "POST" |> ignore + ctx.Request.Path.ReturnsForAnyArgs (PathString("/any")) |> ignore + ctx.Request.Headers.ReturnsForAnyArgs(headers) |> ignore + ctx.Response.Body <- new MemoryStream() + let expected = "first route" + + task { + let! result = app next ctx + + match result with + | None -> assertFail $"Result was expected to be %s{expected}" + | Some ctx -> + let body = getBody ctx + Assert.Equal(expected, body) + Assert.Equal("text/plain; charset=utf-8", ctx.Response |> getContentType) + } + +[] +let ``POST with an accept header type containing a fuzzy type and concrete subtype returns the first matching route`` () = + /// Reference: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + let ctx = Substitute.For() + let app = + choose [ + POST >=> choose [ + route "/any" >=> mustAccept [ "text/plain" ] >=> text "first route" + route "/any" >=> mustAccept [ "application/xml" ] >=> text "second route" + route "/any" >=> mustAccept [ "text/plain"; "application/json" ] >=> text "third route" ] + setStatusCode 404 >=> text "Not found" ] + + let headers = HeaderDictionary() + headers.Add("Accept", StringValues("application/*")) + ctx.Request.Method.ReturnsForAnyArgs "POST" |> ignore + ctx.Request.Path.ReturnsForAnyArgs (PathString("/any")) |> ignore + ctx.Request.Headers.ReturnsForAnyArgs(headers) |> ignore + ctx.Response.Body <- new MemoryStream() + let expected = "second route" + + task { + let! result = app next ctx + + match result with + | None -> assertFail $"Result was expected to be %s{expected}" + | Some ctx -> + let body = getBody ctx + Assert.Equal(expected, body) + Assert.Equal("text/plain; charset=utf-8", ctx.Response |> getContentType) + } + [] let ``GET "/person" returns rendered HTML view`` () = let ctx = Substitute.For()