├── tests └── Giraffe.Tests │ ├── xunit.runner.json │ ├── TestFiles │ ├── streaming.txt │ ├── streaming2.txt │ └── index.html │ ├── DateTimeTests.fs │ ├── ResponseCachingTests.fs │ ├── Giraffe.Tests.fsproj │ ├── GuidAndIdTests.fs │ ├── ModelValidationTests.fs │ ├── XmlTests.fs │ ├── Helpers.fs │ ├── FormatExpressionTests.fs │ └── EndpointRoutingTests.fs ├── giraffe.png ├── giraffe-64x64.png ├── .gitattributes ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md ├── dependabot.yml └── workflows │ ├── fantomas-check.yml │ ├── build-and-test.yml │ └── publish.yml ├── NuGet.config ├── Directory.Solution.targets ├── .config └── dotnet-tools.json ├── MAINTAINERS.md ├── samples ├── RateLimiting │ ├── RateLimiting.fsproj │ ├── README.md │ ├── rate-limiting-test.fsx │ └── Program.fs ├── GlobalRateLimiting │ ├── GlobalRateLimiting.fsproj │ ├── global-rate-limiting-test.fsx │ ├── README.md │ └── Program.fs ├── EndpointRoutingApp │ ├── EndpointRoutingApp.fsproj │ └── Program.fs ├── ResponseCachingApp │ ├── ResponseCachingApp.fsproj │ ├── Program.fs │ ├── test-run.fsx │ └── README.md └── NewtonsoftJson │ ├── NewtonsoftJson.fsproj │ └── Program.fs ├── SECURITY.md ├── Directory.Build.targets ├── src └── Giraffe │ ├── ComputationExpressions.fs │ ├── ModelValidation.fs │ ├── DateTimeExtensions.fs │ ├── Xml.fs │ ├── Json.fs │ ├── Helpers.fs │ ├── HttpStatusCodeHandlers.fs │ ├── ShortGuid.fs │ ├── Giraffe.fsproj │ ├── ResponseCaching.fs │ ├── Middleware.fs │ ├── Csrf.fs │ ├── RequestLimitation.fs │ ├── Auth.fs │ ├── Negotiation.fs │ ├── Preconditional.fs │ └── FormatExpressions.fs ├── .vscode ├── tasks.json └── launch.json ├── DEVGUIDE.md ├── CODE_OF_CONDUCT.md ├── .gitignore ├── Giraffe.sln └── LICENSE /tests/Giraffe.Tests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "appDomain": "denied" 3 | } -------------------------------------------------------------------------------- /giraffe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giraffe-fsharp/Giraffe/master/giraffe.png -------------------------------------------------------------------------------- /giraffe-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giraffe-fsharp/Giraffe/master/giraffe-64x64.png -------------------------------------------------------------------------------- /tests/Giraffe.Tests/TestFiles/streaming.txt: -------------------------------------------------------------------------------- 1 | 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ -------------------------------------------------------------------------------- /tests/Giraffe.Tests/TestFiles/streaming2.txt: -------------------------------------------------------------------------------- 1 | 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf 3 | 4 | # Always use lf for F# files 5 | *.fs text eol=lf 6 | *.fsx text eol=lf 7 | *.fsi text eol=lf -------------------------------------------------------------------------------- /tests/Giraffe.Tests/TestFiles/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Giraffe Html Testpage 5 | 6 | 7 |

Test page

8 | 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{fs,fsi,fsx}] 4 | end_of_line = lf 5 | fsharp_multiline_bracket_style = aligned 6 | fsharp_multi_line_lambda_closing_newline = true 7 | 8 | [{tests,samples}/**/*.fs] 9 | fsharp_experimental_elmish = true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Use this template to report issues that do not fit other predefined categories. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Directory.Solution.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## How to test 6 | 7 | 8 | 9 | ## Related issues 10 | 11 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "fantomas": { 6 | "version": "7.0.5", 7 | "commands": [ 8 | "fantomas" 9 | ] 10 | }, 11 | "fsharp-analyzers": { 12 | "version": "0.33.1", 13 | "commands": [ 14 | "fsharp-analyzers" 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | ## Current maintainers of the project 2 | 3 | | Maintainer | GitHub ID | 4 | | --------------------| ----------------------------------------| 5 | | Vinícius Gajo | [64J0](https://github.com/64J0) | 6 | | Dag Brattli | [dbrattli](https://github.com/dbrattli) | 7 | | Moritz Jörg | [mrtz-j](https://github.com/mrtz-j) | -------------------------------------------------------------------------------- /samples/RateLimiting/RateLimiting.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/GlobalRateLimiting/GlobalRateLimiting.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/EndpointRoutingApp/EndpointRoutingApp.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/ResponseCachingApp/ResponseCachingApp.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Security updates are generally only applied to the latest version of Giraffe unless the nature and impact of the vulnerability requires otherwise. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Please report critical security vulnerabilities directly to [Dustin Gorski](https://twitter.com/dustinmoris) or [someone from the core maintainers](https://github.com/orgs/giraffe-fsharp/people). 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic set up 2 | 3 | version: 2 4 | updates: 5 | 6 | # Maintain dependencies for GitHub Actions 7 | - package-ecosystem: "github-actions" 8 | # Workflow files stored in the default location of `.github/workflows`. (You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`.) 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | 13 | # Maintain dependencies for nuget 14 | - package-ecosystem: "nuget" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | -------------------------------------------------------------------------------- /samples/NewtonsoftJson/NewtonsoftJson.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/GlobalRateLimiting/global-rate-limiting-test.fsx: -------------------------------------------------------------------------------- 1 | open System 2 | open System.IO 3 | open System.Net.Http 4 | 5 | let request = new HttpClient(BaseAddress = new Uri("http://localhost:5000")) 6 | 7 | #time 8 | 9 | seq { 1..100 } 10 | |> Seq.map (fun _ -> request.GetAsync "/" |> Async.AwaitTask) 11 | |> Async.Parallel 12 | |> Async.RunSynchronously 13 | |> Seq.iteri (fun i response -> 14 | printfn "\nResponse %i status code: %A" i response.StatusCode 15 | 16 | let responseReader = new StreamReader(response.Content.ReadAsStream()) 17 | printfn "Response %i content: %A" i (responseReader.ReadToEnd()) 18 | ) 19 | 20 | #time 21 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | --analyzers-path "$(PkgG-Research_FSharp_Analyzers)" 4 | $(FSharpAnalyzersOtherFlags) --analyzers-path "$(PkgIonide_Analyzers)" 5 | $(FSharpAnalyzersOtherFlags) --configuration $(Configuration) 6 | $(FSharpAnalyzersOtherFlags) --exclude-analyzers PartialAppAnalyzer 7 | $(FSharpAnalyzersOtherFlags) --report "analysis.sarif" 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/fantomas-check.yml: -------------------------------------------------------------------------------- 1 | name: Fantomas check 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'samples/**' 7 | - 'src/**' 8 | - 'tests/**' 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | fantomas-check: 16 | name: Code format check 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v6 21 | - name: Restore packages 22 | run: dotnet tool restore 23 | - name: Run Fantomas 24 | run: dotnet fantomas --check src samples tests 25 | - name: log failure 26 | if: failure() 27 | run: echo "Some files need formatting, please run 'dotnet fantomas src samples tests'" 28 | -------------------------------------------------------------------------------- /samples/GlobalRateLimiting/README.md: -------------------------------------------------------------------------------- 1 | # Global Rate Limiting Sample 2 | 3 | This sample project shows how one can configure ASP.NET's built-in [rate limiting middleware](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-8.0). 4 | 5 | Notice that this rate limiting configuration is very simple, and for real life scenarios you'll need to figure out what is the best strategy to use for your server. 6 | 7 | To make it easier to test this project locally, and see the rate limiting middleware working, you can use the `global-rate-limiting-test.fsx` script: 8 | 9 | ```bash 10 | # start the server 11 | dotnet run . 12 | # if you want to keep using the same terminal, just start this process in the background 13 | 14 | # then, you can use this script to test the server, and confirm that the rate-limiting 15 | # middleware is really working 16 | dotnet fsi global-rate-limiting-test.fsx 17 | ``` -------------------------------------------------------------------------------- /src/Giraffe/ComputationExpressions.fs: -------------------------------------------------------------------------------- 1 | /// 2 | /// A collection of F# computation expressions: 3 | /// 4 | /// `opt {}`: Enables control flow and binding of Option{T} objects 5 | /// `res {}`: Enables control flow and binding of Result{T, TError} objects 6 | /// 7 | module Giraffe.ComputationExpressions 8 | 9 | /// 10 | /// Enables control flow and binding of `Option{T}` objects 11 | /// 12 | type OptionBuilder() = 13 | member __.Bind(v, f) = Option.bind f v 14 | member __.Return v = Some v 15 | member __.ReturnFrom v = v 16 | member __.Zero() = None 17 | 18 | let opt = OptionBuilder() 19 | 20 | /// 21 | /// Enables control flow and binding of objects 22 | /// 23 | type ResultBuilder() = 24 | member __.Bind(v, f) = Result.bind f v 25 | member __.Return v = Ok v 26 | 27 | let res = ResultBuilder() 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | 13 | ## Expected behavior 14 | 15 | 16 | ## To Reproduce 17 | 19 | 20 | ## Exceptions 21 | 22 | 23 | ```shell 24 | 25 | ``` 26 | 27 | ## Environment: 28 | 29 | 30 | - OS: [e.g. Windows 10, macOS, Linux (distro)] 31 | - .NET version: [e.g. 9.0.101] 32 | 33 | ## Additional context 34 | -------------------------------------------------------------------------------- /samples/RateLimiting/README.md: -------------------------------------------------------------------------------- 1 | # Rate Limiting Sample 2 | 3 | This sample project shows how one can configure ASP.NET's built-in [rate limiting middleware](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-8.0). 4 | 5 | Notice that this rate limiting configuration is very simple, and for real life scenarios you'll need to figure out what is the best strategy to use for your server. 6 | 7 | To make it easier to test this project locally, and see the rate limiting middleware working, you can use the `rate-limiting-test.fsx` script: 8 | 9 | ```bash 10 | # start the server 11 | dotnet run . 12 | # if you want to keep using the same terminal, just start this process in the background 13 | 14 | # then, you can use this script to test the server, and confirm that the rate-limiting 15 | # middleware is really working 16 | dotnet fsi rate-limiting-test.fsx 17 | 18 | # to run with the DEBUG flag active 19 | dotnet fsi --define:DEBUG rate-limiting-test.fsx 20 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | 13 | 14 | ## Describe the solution you'd like 15 | 16 | 17 | ## Describe alternatives you've considered 18 | 20 | 21 | ## Additional context 22 | 30 | 31 | - [ ] I would like to contribute to the implementation of this feature 32 | - [ ] I would like to sponsor the development of this feature -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build EndpointRoutingApp", 8 | "command": "dotnet build samples/EndpointRoutingApp/EndpointRoutingApp.fsproj", 9 | "type": "shell", 10 | "group": "build", 11 | "presentation": { 12 | "reveal": "silent" 13 | }, 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "build NewtonsoftJson", 18 | "command": "dotnet build samples/NewtonsoftJson/NewtonsoftJson.fsproj", 19 | "type": "shell", 20 | "group": "build", 21 | "presentation": { 22 | "reveal": "silent" 23 | }, 24 | "problemMatcher": "$msCompile" 25 | }, 26 | { 27 | "label": "build ResponseCachingApp", 28 | "command": "dotnet build samples/ResponseCachingApp/ResponseCachingApp.fsproj", 29 | "type": "shell", 30 | "group": "build", 31 | "presentation": { 32 | "reveal": "silent" 33 | }, 34 | "problemMatcher": "$msCompile" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /tests/Giraffe.Tests/DateTimeTests.fs: -------------------------------------------------------------------------------- 1 | module Giraffe.Tests.DateTimeTests 2 | 3 | open System 4 | open Xunit 5 | open Giraffe 6 | 7 | // --------------------------------- 8 | // DateTime Tests 9 | // --------------------------------- 10 | 11 | [] 12 | let ``DateTime.ToHtmlString() produces a RFC822 formatted string`` () = 13 | let htmlString = 14 | (new DateTime(2019, 01, 01, 0, 0, 0, 0, DateTimeKind.Utc)).ToHtmlString() 15 | 16 | Assert.Equal("Tue, 01 Jan 2019 00:00:00 GMT", htmlString) 17 | 18 | [] 19 | let ``DateTime.ToIsoString() produces a RFC3339 formatted string`` () = 20 | let isoString = 21 | (new DateTime(2019, 01, 01, 0, 0, 0, 0, DateTimeKind.Utc)).ToIsoString() 22 | 23 | Assert.Equal("2019-01-01T00:00:00.0000000Z", isoString) 24 | 25 | [] 26 | let ``DateTimeOffset.ToHtmlString() produces a RFC822 formatted string`` () = 27 | let htmlString = 28 | (new DateTimeOffset(2019, 01, 01, 0, 0, 0, 0, TimeSpan.Zero)).ToHtmlString() 29 | 30 | Assert.Equal("Tue, 01 Jan 2019 00:00:00 GMT", htmlString) 31 | 32 | [] 33 | let ``DateTimeOffset.ToIsoString() produces a RFC3339 formatted string`` () = 34 | let isoString = 35 | (new DateTimeOffset(2019, 01, 01, 0, 0, 0, 0, TimeSpan.Zero)).ToIsoString() 36 | 37 | Assert.Equal("2019-01-01T00:00:00.0000000+00:00", isoString) 38 | -------------------------------------------------------------------------------- /src/Giraffe/ModelValidation.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe 2 | 3 | [] 4 | module ModelValidation = 5 | 6 | /// 7 | /// Interface defining model validation methods. 8 | /// 9 | type IModelValidation<'T> = 10 | /// 11 | /// Contract for validating an object's state. 12 | /// 13 | /// If the object has a valid state then the function should return the object, otherwise it should return a `HttpHandler` function which is ought to return an error response back to a client. 14 | /// 15 | abstract member Validate: unit -> Result<'T, HttpHandler> 16 | 17 | /// 18 | /// Validates an object of type 'T where 'T must have implemented interface . 19 | /// 20 | /// If validation was successful then object 'T will be passed into the function "f", otherwise an error response will be sent back to the client. 21 | /// 22 | /// A function which accepts the model 'T and returns a function. 23 | /// An instance of type 'T, where 'T must implement interface . 24 | /// 25 | /// A Giraffe function which can be composed into a bigger web application. 26 | let validateModel<'T when 'T :> IModelValidation<'T>> (f: 'T -> HttpHandler) (model: 'T) : HttpHandler = 27 | match model.Validate() with 28 | | Ok _ -> f model 29 | | Error err -> err 30 | -------------------------------------------------------------------------------- /tests/Giraffe.Tests/ResponseCachingTests.fs: -------------------------------------------------------------------------------- 1 | module Giraffe.Tests.ResponseCachingTests 2 | 3 | open System 4 | open System.IO 5 | open Microsoft.AspNetCore.Http 6 | open Microsoft.Net.Http.Headers 7 | open Microsoft.Extensions.Primitives 8 | open Giraffe 9 | open NSubstitute 10 | open Xunit 11 | 12 | // --------------------------------- 13 | // Request caching tests 14 | // --------------------------------- 15 | 16 | [] 17 | let ``responseCaching is updating 'vary' response header`` () = 18 | let ctx = Substitute.For() 19 | 20 | let responseCachingMiddleware: HttpHandler = 21 | responseCaching 22 | (Public(TimeSpan.FromSeconds(float 30))) 23 | (Some "Accept, Accept-Encoding") 24 | (Some [| "query1"; "query2" |]) 25 | 26 | let app = 27 | GET 28 | >=> route "/ok" 29 | >=> responseCachingMiddleware 30 | >=> setStatusCode 200 31 | >=> text "ok" 32 | 33 | ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore 34 | ctx.Request.Path.ReturnsForAnyArgs("/ok") |> ignore 35 | ctx.Response.Headers.ReturnsForAnyArgs(new HeaderDictionary()) |> ignore 36 | ctx.Response.Body <- new MemoryStream() 37 | 38 | task { 39 | let! result = app next ctx 40 | 41 | match result with 42 | | None -> assertFail "Non expected result" 43 | | Some ctx -> 44 | Assert.Equal(StatusCodes.Status200OK, ctx.Response.StatusCode) 45 | 46 | let expectedVaryHeader = StringValues [| "Accept, Accept-Encoding" |] |> string 47 | Assert.Equal(string ctx.Response.Headers.[HeaderNames.Vary], expectedVaryHeader) 48 | 49 | let expectedBody = "ok" 50 | Assert.Equal(expectedBody, getBody ctx) 51 | } 52 | -------------------------------------------------------------------------------- /samples/RateLimiting/rate-limiting-test.fsx: -------------------------------------------------------------------------------- 1 | open System 2 | open System.Net.Http 3 | 4 | let request = new HttpClient(BaseAddress = new Uri("http://localhost:5000")) 5 | 6 | let program () = 7 | async { 8 | let! reqResult1 = 9 | seq { 1..100 } 10 | |> Seq.map (fun _ -> request.GetAsync "/no-rate-limit" |> Async.AwaitTask) 11 | |> Async.Parallel 12 | 13 | reqResult1 14 | #if DEBUG 15 | |> Seq.iteri (fun i response -> 16 | printfn "\nResponse %i status code: %A" i response.StatusCode 17 | 18 | let responseReader = new StreamReader(response.Content.ReadAsStream()) 19 | printfn "Response %i content: %A" i (responseReader.ReadToEnd()) 20 | ) 21 | #else 22 | |> Seq.groupBy (fun response -> response.StatusCode) 23 | |> Seq.iter (fun (group) -> 24 | let key, seqRes = group 25 | printfn "Quantity of requests with status code %A: %i" (key) (Seq.length seqRes) 26 | ) 27 | #endif 28 | 29 | printfn "\nWith rate limit now...\n" 30 | 31 | let! reqResult2 = 32 | seq { 1..100 } 33 | |> Seq.map (fun _ -> request.GetAsync "/rate-limit" |> Async.AwaitTask) 34 | |> Async.Parallel 35 | 36 | reqResult2 37 | #if DEBUG 38 | |> Seq.iteri (fun i response -> 39 | printfn "\nResponse %i status code: %A" i response.StatusCode 40 | 41 | let responseReader = new StreamReader(response.Content.ReadAsStream()) 42 | printfn "Response %i content: %A" i (responseReader.ReadToEnd()) 43 | ) 44 | #else 45 | |> Seq.groupBy (fun response -> response.StatusCode) 46 | |> Seq.iter (fun (group) -> 47 | let key, seqRes = group 48 | printfn "Quantity of requests with status code %A: %i\n" (key) (Seq.length seqRes) 49 | ) 50 | #endif 51 | } 52 | 53 | #time 54 | 55 | program () |> Async.RunSynchronously 56 | 57 | #time 58 | -------------------------------------------------------------------------------- /src/Giraffe/DateTimeExtensions.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe 2 | 3 | open System 4 | open System.Runtime.CompilerServices 5 | 6 | [] 7 | type DateTimeExtensions() = 8 | 9 | /// 10 | /// Converts a object into an RFC822 formatted . 11 | /// 12 | /// Using specification https://www.ietf.org/rfc/rfc822.txt 13 | /// Formatted string value. 14 | [] 15 | static member inline ToHtmlString(dt: DateTime) = dt.ToString("r") 16 | 17 | /// 18 | /// Converts a object into an RFC3339 formatted . 19 | /// 20 | /// Using specification https://www.ietf.org/rfc/rfc3339.txt 21 | /// Formatted string value. 22 | [] 23 | static member inline ToIsoString(dt: DateTime) = dt.ToString("o") 24 | 25 | /// 26 | /// Converts a object into an RFC822 formatted . 27 | /// 28 | /// Using specification https://www.ietf.org/rfc/rfc822.txt 29 | /// Formatted string value. 30 | [] 31 | static member inline ToHtmlString(dt: DateTimeOffset) = dt.ToString("r") 32 | 33 | /// 34 | /// Converts a object into an RFC3339 formatted . 35 | /// 36 | /// Using specification https://www.ietf.org/rfc/rfc3339.txt 37 | /// Formatted string value. 38 | [] 39 | static member inline ToIsoString(dt: DateTimeOffset) = dt.ToString("o") 40 | 41 | [] 42 | static member inline CutOffMs(dt: DateTimeOffset) = 43 | DateTimeOffset(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, 0, dt.Offset) 44 | -------------------------------------------------------------------------------- /samples/GlobalRateLimiting/Program.fs: -------------------------------------------------------------------------------- 1 | open System 2 | open Microsoft.AspNetCore.Http 3 | open Microsoft.AspNetCore.Builder 4 | open Microsoft.Extensions.DependencyInjection 5 | open Microsoft.Extensions.Hosting 6 | open Giraffe 7 | open Giraffe.EndpointRouting 8 | open Microsoft.AspNetCore.RateLimiting 9 | open System.Threading.RateLimiting 10 | 11 | let endpoints: list = [ GET [ route "/" (text "Hello World") ] ] 12 | 13 | let notFoundHandler = text "Not Found" |> RequestErrors.notFound 14 | 15 | let configureApp (appBuilder: IApplicationBuilder) = 16 | appBuilder.UseRouting().UseRateLimiter().UseGiraffe(endpoints).UseGiraffe(notFoundHandler) 17 | 18 | let configureServices (services: IServiceCollection) = 19 | // From https://blog.maartenballiauw.be/post/2022/09/26/aspnet-core-rate-limiting-middleware.html 20 | let configureRateLimiter (options: RateLimiterOptions) = 21 | options.RejectionStatusCode <- StatusCodes.Status429TooManyRequests 22 | 23 | options.GlobalLimiter <- 24 | PartitionedRateLimiter.Create(fun httpContext -> 25 | RateLimitPartition.GetFixedWindowLimiter( 26 | partitionKey = httpContext.Request.Headers.Host.ToString(), 27 | factory = 28 | (fun _partition -> 29 | new FixedWindowRateLimiterOptions( 30 | AutoReplenishment = true, 31 | PermitLimit = 10, 32 | QueueLimit = 0, 33 | Window = TimeSpan.FromSeconds(1) 34 | ) 35 | ) 36 | ) 37 | ) 38 | 39 | services.AddRateLimiter(configureRateLimiter).AddRouting().AddGiraffe() 40 | |> ignore 41 | 42 | [] 43 | let main args = 44 | let builder = WebApplication.CreateBuilder(args) 45 | configureServices builder.Services 46 | 47 | let app = builder.Build() 48 | 49 | configureApp app 50 | app.Run() 51 | 52 | 0 53 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch EndpointRoutingApp (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build EndpointRoutingApp", 12 | "program": "${workspaceFolder}/samples/EndpointRoutingApp/bin/Debug/net8.0/EndpointRoutingApp.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}", 15 | "console": "internalConsole", 16 | "internalConsoleOptions": "openOnSessionStart", 17 | "stopAtEntry": false, 18 | "justMyCode": false 19 | }, 20 | { 21 | "name": ".NET Core Launch NewtonsoftJson (console)", 22 | "type": "coreclr", 23 | "request": "launch", 24 | "preLaunchTask": "build NewtonsoftJson", 25 | "program": "${workspaceFolder}/samples/NewtonsoftJson/bin/Debug/net8.0/NewtonsoftJson.dll", 26 | "args": [], 27 | "cwd": "${workspaceFolder}", 28 | "console": "internalConsole", 29 | "internalConsoleOptions": "openOnSessionStart", 30 | "stopAtEntry": false, 31 | "justMyCode": false 32 | }, 33 | { 34 | "name": ".NET Core Launch ResponseCachingApp (console)", 35 | "type": "coreclr", 36 | "request": "launch", 37 | "preLaunchTask": "build ResponseCachingApp", 38 | "program": "${workspaceFolder}/samples/ResponseCachingApp/bin/Debug/net7.0/ResponseCachingApp.dll", 39 | "args": [], 40 | "cwd": "${workspaceFolder}", 41 | "console": "internalConsole", 42 | "internalConsoleOptions": "openOnSessionStart", 43 | "stopAtEntry": false, 44 | "justMyCode": false 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /samples/RateLimiting/Program.fs: -------------------------------------------------------------------------------- 1 | open System 2 | open Microsoft.AspNetCore.Http 3 | open Microsoft.AspNetCore.Builder 4 | open Microsoft.Extensions.DependencyInjection 5 | open Microsoft.Extensions.Hosting 6 | open Giraffe 7 | open Giraffe.EndpointRouting 8 | open Microsoft.AspNetCore.RateLimiting 9 | open System.Threading.RateLimiting 10 | 11 | let MY_RATE_LIMITER = "fixed" 12 | 13 | let endpoints: list = 14 | [ 15 | GET [ 16 | routeWithExtensions (fun eb -> eb.RequireRateLimiting MY_RATE_LIMITER) "/rate-limit" (text "Hello World") 17 | route "/no-rate-limit" (text "Hello World: No Rate Limit!") 18 | ] 19 | ] 20 | 21 | let notFoundHandler = text "Not Found" |> RequestErrors.notFound 22 | 23 | let configureApp (appBuilder: IApplicationBuilder) = 24 | appBuilder.UseRouting().UseRateLimiter().UseGiraffe(endpoints).UseGiraffe(notFoundHandler) 25 | 26 | let configureServices (services: IServiceCollection) = 27 | // From https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-8.0#fixed-window-limiter 28 | let configureRateLimiter (rateLimiterOptions: RateLimiterOptions) = 29 | rateLimiterOptions.RejectionStatusCode <- StatusCodes.Status429TooManyRequests 30 | 31 | rateLimiterOptions.AddFixedWindowLimiter( 32 | policyName = MY_RATE_LIMITER, 33 | configureOptions = 34 | (fun (options: FixedWindowRateLimiterOptions) -> 35 | options.PermitLimit <- 10 36 | options.Window <- TimeSpan.FromSeconds(int64 12) 37 | options.QueueProcessingOrder <- QueueProcessingOrder.OldestFirst 38 | options.QueueLimit <- 1 39 | ) 40 | ) 41 | |> ignore 42 | 43 | services.AddRateLimiter(configureRateLimiter).AddRouting().AddGiraffe() 44 | |> ignore 45 | 46 | [] 47 | let main args = 48 | let builder = WebApplication.CreateBuilder(args) 49 | configureServices builder.Services 50 | 51 | let app = builder.Build() 52 | 53 | configureApp app 54 | app.Run() 55 | 56 | 0 57 | -------------------------------------------------------------------------------- /DEVGUIDE.md: -------------------------------------------------------------------------------- 1 | # DEVGUIDE 2 | 3 | This documentation must be used as a guide for maintainers and developers for building and releasing this project. 4 | 5 | ## Release Process 6 | 7 | 1. Checkout `master` branch 8 | 1. `git checkout master` 9 | 2. Add a new entry at the top of the RELEASE_NOTES.md with a version and a date. 10 | 1. If possible link to the relevant issues and PRs and credit the author of the PRs 11 | 3. Update the **Version** attribute at *src/Giraffe.fsproj*, using the same version defined at the RELEASE_NOTES.md. 12 | 1. Notice that this can be automated in the future with [ionide/KeepAChangelog](https://github.com/ionide/KeepAChangelog). 13 | 4. Create a new commit 14 | 1. `git add RELEASE_NOTES.md` 15 | 2. `git commit -m "Release 6.0.0-beta001"`. Notice that the pre-release versioning scheme is semantic versioning (SemVer), so each section of the pre-release part is compared separately, and purely-numeric sections are compared as integers. For example, if you eventually release `v6.4.1-alpha-9` and `v6.4.1-alpha-10`, the `alpha-9` version will be ranked higher on Nuget. Keep this in mind and prefer the alphaXXX/betaXXX structure, where XXX are integers starting from 001 to 999. First mentioned at this PR comment: [link](https://github.com/giraffe-fsharp/Giraffe/pull/596#issuecomment-2111097042). 16 | 5. Make a new tag 17 | 1. `git tag v6.0.0-beta001` 18 | 6. Push changes 19 | 1. `git push --atomic origin master v6.0.0-beta001` 20 | 7. Create a [new pre-release](https://github.com/giraffe-fsharp/Giraffe/releases) on GitHub 21 | 1. Choose the tag you just pushed 22 | 2. Title the pre-release the same as the version 23 | 3. Copy the pre-release notes from RELEASE_NOTES.md 24 | 4. This will trigger a github action to build and publish the nuget package 25 | 8. Do any additional testing or tell certain people to try it out 26 | 9. Once satisfied repeat the process but without any alpha/beta/rc suffixes. 27 | 1. Run through steps 2-7, creating a **release** instead of a pre-release 28 | 10. Tell the internet about it 29 | 1. Tweet about it 30 | 2. Post in F# Slack 31 | 3. Post in F# Discord 32 | 11. Celebrate 🎉 33 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | env: 10 | # Stop wasting time caching packages 11 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 12 | # Disable sending usage data to Microsoft 13 | DOTNET_CLI_TELEMETRY_OPTOUT: true 14 | 15 | # Kill other jobs when we trigger this workflow by sending new commits 16 | # to the PR. 17 | # https://stackoverflow.com/a/72408109 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | build: 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | matrix: 27 | os: [ ubuntu-latest, windows-latest, macos-latest ] 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v6 31 | - name: Setup .NET Core 32 | uses: actions/setup-dotnet@v5 33 | with: 34 | dotnet-version: | 35 | 8.x 36 | 9.x 37 | 10.x 38 | - name: Restore 39 | run: dotnet restore 40 | - name: Build 41 | run: dotnet build -c Release --no-restore 42 | - name: Test 43 | run: dotnet test -c Release 44 | 45 | analyzers: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v6 50 | - name: Setup .NET Core 51 | uses: actions/setup-dotnet@v5 52 | with: 53 | dotnet-version: | 54 | 8.x 55 | 9.x 56 | 10.x 57 | - name: Restore tools 58 | run: dotnet tool restore 59 | - name: Build solution 60 | run: dotnet build -c Release Giraffe.sln 61 | 62 | - name: Run Analyzers 63 | run: dotnet msbuild /t:AnalyzeFSharpProject src/Giraffe/Giraffe.fsproj 64 | # This is important, you want to continue your Action even if you found problems. 65 | # As you always want the report to upload 66 | continue-on-error: true 67 | # checkout code, build, run analyzers, ... 68 | - name: Upload SARIF file 69 | uses: github/codeql-action/upload-sarif@v4 70 | with: 71 | # You can also specify the path to a folder for `sarif_file` 72 | sarif_file: ./src/Giraffe/analysis.sarif 73 | -------------------------------------------------------------------------------- /src/Giraffe/Xml.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe 2 | 3 | [] 4 | module Xml = 5 | /// 6 | /// Interface defining XML serialization methods. 7 | /// Use this interface to customize XML serialization in Giraffe. 8 | /// 9 | type ISerializer = 10 | abstract member Serialize: obj -> byte array 11 | abstract member Deserialize<'T> : string -> 'T 12 | 13 | [] 14 | module SystemXml = 15 | open Microsoft.IO 16 | open System.Text 17 | open System.IO 18 | open System.Xml 19 | open System.Xml.Serialization 20 | 21 | /// 22 | /// Default XML serializer in Giraffe. 23 | /// Serializes objects to UTF8 encoded indented XML code. 24 | /// 25 | type Serializer(settings: XmlWriterSettings, rmsManager: RecyclableMemoryStreamManager) = 26 | 27 | new(settings: XmlWriterSettings) = Serializer(settings, recyclableMemoryStreamManager.Value) 28 | 29 | static member DefaultSettings = 30 | XmlWriterSettings(Encoding = Encoding.UTF8, Indent = true, OmitXmlDeclaration = false) 31 | 32 | interface Xml.ISerializer with 33 | member __.Serialize(o: obj) = 34 | use stream = 35 | if rmsManager.Settings.ThrowExceptionOnToArray then 36 | new MemoryStream() 37 | else 38 | rmsManager.GetStream("giraffe-xml-serialize") 39 | 40 | use writer = XmlWriter.Create(stream, settings) 41 | let serializer = XmlSerializer(o.GetType()) 42 | serializer.Serialize(writer, o) 43 | stream.ToArray() 44 | 45 | member __.Deserialize<'T>(xml: string) = 46 | let serializer = XmlSerializer(typeof<'T>) 47 | use stringReader = new StringReader(xml) 48 | // Secure XML parsing: disable DTD processing and external entities to prevent XXE attacks 49 | let xmlReaderSettings = 50 | new XmlReaderSettings( 51 | DtdProcessing = DtdProcessing.Prohibit, 52 | XmlResolver = null, 53 | MaxCharactersFromEntities = 1024L * 1024L 54 | ) // 1MB limit 55 | 56 | use xmlReader = XmlReader.Create(stringReader, xmlReaderSettings) 57 | serializer.Deserialize xmlReader :?> 'T 58 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | env: 9 | # Stop wasting time caching packages 10 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 11 | # Disable sending usage data to Microsoft 12 | DOTNET_CLI_TELEMETRY_OPTOUT: true 13 | # Project name to pack and publish 14 | PROJECT_NAME: Giraffe 15 | # GitHub Packages Feed settings 16 | GITHUB_FEED: https://nuget.pkg.github.com/giraffe-fsharp/ 17 | GITHUB_USER: dustinmoris 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | # Official NuGet Feed settings 20 | NUGET_FEED: https://api.nuget.org/v3/index.json 21 | NUGET_KEY: ${{ secrets.NUGET_KEY }} 22 | 23 | jobs: 24 | build: 25 | name: Build the project 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | matrix: 29 | os: [ ubuntu-latest, windows-latest, macos-latest ] 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v6 33 | - name: Setup .NET Core 34 | uses: actions/setup-dotnet@v5 35 | with: 36 | dotnet-version: | 37 | 8.x 38 | 9.x 39 | 10.x 40 | - name: Restore 41 | run: dotnet restore 42 | - name: Build 43 | run: dotnet build -c Release --no-restore 44 | - name: Test 45 | run: dotnet test -c Release 46 | 47 | deploy: 48 | name: Publish a new version 49 | needs: build 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v6 53 | - name: Setup .NET Core 54 | uses: actions/setup-dotnet@v5 55 | with: 56 | dotnet-version: | 57 | 8.x 58 | 9.x 59 | 10.x 60 | - name: Create Release NuGet package 61 | run: | 62 | arrTag=(${GITHUB_REF//\// }) 63 | VERSION="${arrTag[2]}" 64 | echo Version: $VERSION 65 | 66 | VERSION="${VERSION//v}" 67 | echo Clean Version: $VERSION 68 | 69 | dotnet pack -v normal -c Release --include-symbols --include-source -p:PackageVersion=$VERSION -o nupkg src/$PROJECT_NAME/$PROJECT_NAME.*proj 70 | - name: Push to GitHub Feed 71 | run: | 72 | for f in ./nupkg/*.nupkg 73 | do 74 | echo $f 75 | curl -vX PUT -u "$GITHUB_USER:$GITHUB_TOKEN" -F package=@$f $GITHUB_FEED 76 | done 77 | - name: Push to NuGet Feed 78 | run: dotnet nuget push ./nupkg/*.nupkg --source $NUGET_FEED --skip-duplicate --api-key $NUGET_KEY 79 | -------------------------------------------------------------------------------- /samples/ResponseCachingApp/Program.fs: -------------------------------------------------------------------------------- 1 | open System 2 | open Microsoft.AspNetCore.Builder 3 | open Microsoft.AspNetCore.Http 4 | open Microsoft.Extensions.DependencyInjection 5 | open Microsoft.Extensions.Hosting 6 | open Giraffe 7 | open Giraffe.EndpointRouting 8 | 9 | let expensiveOperation () : DateTime = 10 | let fiveSeconds = 5000 // ms 11 | Threading.Thread.Sleep fiveSeconds 12 | DateTime.Now 13 | 14 | let dateTimeHandler: HttpHandler = 15 | fun (_next: HttpFunc) (ctx: HttpContext) -> 16 | let query1 = ctx.GetQueryStringValue("query1") 17 | let query2 = ctx.GetQueryStringValue("query2") 18 | 19 | let now = expensiveOperation () 20 | setStatusCode 200 |> ignore // for testing purposes 21 | 22 | match (query1, query2) with 23 | | Ok q1, Ok q2 -> ctx.WriteTextAsync $"Parameters: query1 {q1} query2 {q2} -> DateTime: {now}" 24 | | _ -> ctx.WriteTextAsync $"Hello World -> DateTime: {now}" 25 | 26 | let responseCachingMiddleware: HttpHandler = 27 | responseCaching 28 | (Public(TimeSpan.FromSeconds(float 30))) 29 | (Some "Accept, Accept-Encoding") 30 | (Some [| "query1"; "query2" |]) 31 | 32 | let endpoints: Endpoint list = 33 | [ 34 | subRoute "/cached" [ 35 | GET [ 36 | route "/public" (publicResponseCaching 30 None >=> dateTimeHandler) 37 | route "/private" (privateResponseCaching 30 None >=> dateTimeHandler) 38 | route "/not" (noResponseCaching >=> dateTimeHandler) 39 | route "/vary/not" (publicResponseCaching 30 None >=> dateTimeHandler) 40 | route "/vary/yes" (responseCachingMiddleware >=> dateTimeHandler) 41 | ] 42 | ] 43 | ] 44 | 45 | let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound 46 | 47 | let configureServices (services: IServiceCollection) = 48 | services.AddRouting().AddResponseCaching().AddGiraffe() |> ignore 49 | 50 | let configureApp (appBuilder: IApplicationBuilder) = 51 | appBuilder 52 | .UseRouting() 53 | .UseResponseCaching() 54 | .UseEndpoints(fun e -> e.MapGiraffeEndpoints(endpoints)) 55 | .UseGiraffe(notFoundHandler) 56 | 57 | [] 58 | let main args = 59 | let builder = WebApplication.CreateBuilder(args) 60 | configureServices builder.Services 61 | 62 | let app = builder.Build() 63 | 64 | if app.Environment.IsDevelopment() then 65 | app.UseDeveloperExceptionPage() |> ignore 66 | 67 | configureApp app 68 | app.Run() 69 | 70 | 0 71 | -------------------------------------------------------------------------------- /tests/Giraffe.Tests/Giraffe.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0;net10.0 4 | Giraffe.Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | PreserveNewest 31 | 32 | 33 | PreserveNewest 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | all 58 | runtime; build; native; contentfiles; analyzers; buildtransitive 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /samples/ResponseCachingApp/test-run.fsx: -------------------------------------------------------------------------------- 1 | #r "nuget: FsHttp, 11.0.0" 2 | 3 | open System 4 | open FsHttp 5 | 6 | // Uncomment if you don't want FsHttp debug logs 7 | // Fsi.disableDebugLogs() 8 | 9 | type QueryParams = (string * obj) list 10 | type Url = Url of string 11 | type Title = Title of string 12 | 13 | let urls = 14 | {| 15 | notCached = Url "http://localhost:5000/cached/not" 16 | publicCached = Url "http://localhost:5000/cached/public" 17 | privateCached = Url "http://localhost:5000/cached/private" 18 | publicCachedNoVaryByQueryKeys = Url "http://localhost:5000/cached/vary/not" 19 | cachedVaryByQueryKeys = Url "http://localhost:5000/cached/vary/yes" 20 | |} 21 | 22 | let queryParams1: QueryParams = [ ("query1", "a"); ("query2", "b") ] 23 | let queryParams2: QueryParams = [ ("query1", "c"); ("query2", "d") ] 24 | 25 | let waitForOneSecond () = 26 | do Threading.Thread.Sleep(TimeSpan.FromSeconds 1) 27 | 28 | let makeRequest (Url url: Url) (queryParams: QueryParams) = 29 | let response = 30 | http { 31 | GET url 32 | CacheControl "max-age=3600" 33 | query queryParams 34 | } 35 | |> Request.send 36 | |> Response.toFormattedText 37 | 38 | printfn "%s" response 39 | printfn "" 40 | 41 | let printRunTitle (Title title: Title) = 42 | printfn "-----------------------------------" 43 | printfn "%s" title 44 | printfn "" 45 | 46 | let printTimeTaken (duration: TimeSpan) = 47 | printfn "The time it took to finish:" 48 | printfn "%.2f seconds" duration.TotalSeconds 49 | printfn "" 50 | 51 | let run (qps: QueryParams list) (title: Title) (url: Url) = 52 | printRunTitle title 53 | 54 | let stopWatch = Diagnostics.Stopwatch.StartNew() 55 | 56 | for queryParams in qps do 57 | makeRequest url queryParams |> waitForOneSecond 58 | 59 | stopWatch.Stop() 60 | printTimeTaken stopWatch.Elapsed 61 | 62 | let runFiveRequests = 63 | run 64 | [ 65 | for _ in 1..5 do 66 | [] 67 | ] 68 | 69 | let testPublicCachedNoVaryByQueryKeys () = 70 | let allQueryParams = [ queryParams1; queryParams1; queryParams2; queryParams2 ] 71 | let title = Title "Testing the /cached/vary/not endpoint" 72 | let url = urls.publicCachedNoVaryByQueryKeys 73 | run allQueryParams title url 74 | 75 | let testCachedVaryByQueryKeys () = 76 | let allQueryParams = [ queryParams1; queryParams1; queryParams2; queryParams2 ] 77 | let title = Title "Testing the /cached/vary/yes endpoint" 78 | let url = urls.cachedVaryByQueryKeys 79 | run allQueryParams title url 80 | 81 | let main () = 82 | runFiveRequests (Title "Testing the /cached/not endpoint") urls.notCached 83 | runFiveRequests (Title "Testing the /cached/public endpoint") urls.publicCached 84 | runFiveRequests (Title "Testing the /cached/private endpoint") urls.privateCached 85 | testPublicCachedNoVaryByQueryKeys () 86 | testCachedVaryByQueryKeys () 87 | 88 | main () 89 | -------------------------------------------------------------------------------- /samples/EndpointRoutingApp/Program.fs: -------------------------------------------------------------------------------- 1 | open System 2 | open Microsoft.AspNetCore 3 | open Microsoft.AspNetCore.Builder 4 | open Microsoft.AspNetCore.Hosting 5 | open Microsoft.AspNetCore.Http 6 | open Microsoft.Extensions.DependencyInjection 7 | open Microsoft.Extensions.Hosting 8 | open Giraffe 9 | open Giraffe.EndpointRouting 10 | 11 | type Car = 12 | { 13 | Brand: string 14 | Color: string 15 | ReleaseYear: int 16 | } 17 | 18 | let handler1: HttpHandler = 19 | fun (_: HttpFunc) (ctx: HttpContext) -> ctx.WriteTextAsync "Hello World" 20 | 21 | let handler2 (firstName: string, age: int) : HttpHandler = 22 | fun (_: HttpFunc) (ctx: HttpContext) -> 23 | sprintf "Hello %s, you are %i years old." firstName age |> ctx.WriteTextAsync 24 | 25 | let handler3 (a: string, b: string, c: string, d: int) : HttpHandler = 26 | fun (_: HttpFunc) (ctx: HttpContext) -> sprintf "Hello %s %s %s %i" a b c d |> ctx.WriteTextAsync 27 | 28 | let handlerNamed (petId: int) : HttpHandler = 29 | fun (_: HttpFunc) (ctx: HttpContext) -> sprintf "PetId: %i" petId |> ctx.WriteTextAsync 30 | 31 | /// Example request: 32 | /// 33 | /// ```bash 34 | /// curl -v localhost:5000/json -X Post -d '{"brand":"Ford", "color":"Black", "releaseYear":2015}' 35 | /// ``` 36 | let jsonHandler: HttpHandler = 37 | fun (next: HttpFunc) (ctx: HttpContext) -> 38 | task { 39 | match! ctx.BindJsonAsync() with 40 | | { 41 | Brand = _brand 42 | Color = _color 43 | ReleaseYear = releaseYear 44 | } when releaseYear >= 1990 -> return! json {| Message = "Valid car" |} next ctx 45 | | _ -> return! (setStatusCode 400 >=> json {| Message = "Invalid car year" |}) next ctx 46 | } 47 | 48 | let endpoints = 49 | [ 50 | subRoute "/foo" [ GET [ route "/bar" (text "Aloha!") ] ] 51 | GET [ 52 | route "/" (text "Hello World") 53 | routef "/%s/%i" handler2 54 | routef "/%s/%s/%s/%i" handler3 55 | routef "/pet/%i:petId" handlerNamed 56 | ] 57 | GET_HEAD [ 58 | route "/foo" (text "Bar") 59 | route "/x" (text "y") 60 | route "/abc" (text "def") 61 | route "/123" (text "456") 62 | ] 63 | // Not specifying a http verb means it will listen to all verbs 64 | subRoute "/sub" [ route "/test" handler1 ] 65 | POST [ route "/json" jsonHandler ] 66 | ] 67 | 68 | let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound 69 | 70 | let configureApp (appBuilder: IApplicationBuilder) = 71 | appBuilder.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler) 72 | 73 | let configureServices (services: IServiceCollection) = 74 | services.AddRouting().AddGiraffe() |> ignore 75 | 76 | [] 77 | let main args = 78 | let builder = WebApplication.CreateBuilder(args) 79 | configureServices builder.Services 80 | 81 | let app = builder.Build() 82 | 83 | if app.Environment.IsDevelopment() then 84 | app.UseDeveloperExceptionPage() |> ignore 85 | 86 | configureApp app 87 | app.Run() 88 | 89 | 0 90 | -------------------------------------------------------------------------------- /src/Giraffe/Json.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe 2 | 3 | [] 4 | module Json = 5 | open System 6 | open System.IO 7 | open System.Text.Json 8 | open System.Threading.Tasks 9 | open System.Text.Json.Serialization 10 | 11 | /// 12 | /// Interface defining JSON serialization methods. 13 | /// Use this interface to customize JSON serialization in Giraffe. 14 | /// 15 | type ISerializer = 16 | abstract member SerializeToString<'T> : 'T -> string 17 | abstract member SerializeToBytes<'T> : 'T -> byte array 18 | abstract member SerializeToStreamAsync<'T> : 'T -> Stream -> Task 19 | 20 | abstract member Deserialize<'T> : string -> 'T 21 | abstract member Deserialize<'T> : byte[] -> 'T 22 | abstract member DeserializeAsync<'T> : Stream -> Task<'T> 23 | 24 | /// 25 | /// is the default in Giraffe. 26 | /// 27 | /// It uses as the underlying JSON serializer to (de-)serialize 28 | /// JSON content. 29 | /// For support of F# unions and records, look at https://github.com/Tarmil/FSharp.SystemTextJson 30 | /// which plugs into this serializer. 31 | /// 32 | type Serializer(options: JsonSerializerOptions) = 33 | 34 | static member DefaultOptions = 35 | JsonSerializerOptions(PropertyNamingPolicy = JsonNamingPolicy.CamelCase) 36 | 37 | interface ISerializer with 38 | member __.SerializeToString(x: 'T) = JsonSerializer.Serialize(x, options) 39 | 40 | member __.SerializeToBytes(x: 'T) = 41 | JsonSerializer.SerializeToUtf8Bytes(x, options) 42 | 43 | member __.SerializeToStreamAsync (x: 'T) (stream: Stream) = 44 | JsonSerializer.SerializeAsync(stream, x, options) 45 | 46 | member __.Deserialize<'T>(json: string) : 'T = 47 | JsonSerializer.Deserialize<'T>(json, options) 48 | 49 | member __.Deserialize<'T>(bytes: byte array) : 'T = 50 | JsonSerializer.Deserialize<'T>(Span<_>.op_Implicit (bytes.AsSpan()), options) 51 | 52 | member __.DeserializeAsync<'T>(stream: Stream) : Task<'T> = 53 | JsonSerializer.DeserializeAsync<'T>(stream, options).AsTask() 54 | 55 | module FsharpFriendlySerializer = 56 | let DefaultOptions = JsonFSharpOptions.Default() 57 | 58 | let private appendJsonFSharpOptions (fsharpOptions: JsonFSharpOptions) (jsonOptions: JsonSerializerOptions) = 59 | jsonOptions.Converters.Add(JsonFSharpConverter(fsharpOptions)) 60 | jsonOptions 61 | 62 | let buildConfig (fsharpOptions: JsonFSharpOptions option) (jsonOptions: JsonSerializerOptions option) = 63 | jsonOptions 64 | |> Option.defaultValue (JsonSerializerOptions()) 65 | |> appendJsonFSharpOptions (fsharpOptions |> Option.defaultValue DefaultOptions) 66 | 67 | type FsharpFriendlySerializer(?fsharpOptions: JsonFSharpOptions, ?jsonOptions: JsonSerializerOptions) = 68 | inherit Serializer(FsharpFriendlySerializer.buildConfig fsharpOptions jsonOptions) 69 | -------------------------------------------------------------------------------- /src/Giraffe/Helpers.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe 2 | 3 | [] 4 | module Helpers = 5 | open System 6 | open System.IO 7 | open Microsoft.IO 8 | 9 | /// Default single RecyclableMemoryStreamManager. 10 | let internal recyclableMemoryStreamManager = Lazy() 11 | 12 | /// 13 | /// Checks if an object is not null. 14 | /// 15 | /// The object to validate against `null`. 16 | /// Returns true if the object is not null otherwise false. 17 | let inline isNotNull x = not (isNull x) 18 | 19 | /// 20 | /// Converts a string into a string option where null or an empty string will be converted to None and everything else to Some string. 21 | /// 22 | /// The string value to be converted into an option of string. 23 | /// Returns None if the string was null or empty otherwise Some string. 24 | let inline strOption (str: string) = 25 | if String.IsNullOrEmpty str then None else Some str 26 | 27 | /// 28 | /// Reads a file asynchronously from the file system. 29 | /// 30 | /// The absolute path of the file. 31 | /// Returns the string contents of the file wrapped in a Task. 32 | let readFileAsStringAsync (filePath: string) = 33 | task { 34 | use reader = new StreamReader(filePath) 35 | return! reader.ReadToEndAsync() 36 | } 37 | 38 | /// 39 | /// Utility function for matching 1xx HTTP status codes. 40 | /// 41 | /// The HTTP status code. 42 | /// Returns true if the status code is between 100 and 199. 43 | let is1xxStatusCode (statusCode: int) = 100 <= statusCode && statusCode <= 199 44 | 45 | /// 46 | /// Utility function for matching 2xx HTTP status codes. 47 | /// 48 | /// The HTTP status code. 49 | /// Returns true if the status code is between 200 and 299. 50 | let is2xxStatusCode (statusCode: int) = 200 <= statusCode && statusCode <= 299 51 | 52 | /// 53 | /// Utility function for matching 3xx HTTP status codes. 54 | /// 55 | /// The HTTP status code. 56 | /// Returns true if the status code is between 300 and 399. 57 | let is3xxStatusCode (statusCode: int) = 300 <= statusCode && statusCode <= 399 58 | 59 | /// 60 | /// Utility function for matching 4xx HTTP status codes. 61 | /// 62 | /// The HTTP status code. 63 | /// Returns true if the status code is between 400 and 499. 64 | let is4xxStatusCode (statusCode: int) = 400 <= statusCode && statusCode <= 499 65 | 66 | /// 67 | /// Utility function for matching 5xx HTTP status codes. 68 | /// 69 | /// The HTTP status code. 70 | /// Returns true if the status code is between 500 and 599. 71 | let is5xxStatusCode (statusCode: int) = 500 <= statusCode && statusCode <= 599 72 | -------------------------------------------------------------------------------- /tests/Giraffe.Tests/GuidAndIdTests.fs: -------------------------------------------------------------------------------- 1 | module Giraffe.Tests.ShortGuidTests 2 | 3 | open System 4 | open Xunit 5 | open Giraffe 6 | 7 | // --------------------------------- 8 | // Short Guid Tests 9 | // --------------------------------- 10 | 11 | let rndInt64 (rand: Random) = 12 | let buffer = Array.zeroCreate 8 13 | rand.NextBytes buffer 14 | BitConverter.ToUInt64(buffer, 0) 15 | 16 | [] 17 | let ``Short Guids translate to correct long Guids`` () = 18 | let testCases = 19 | [ 20 | "FEx1sZbSD0ugmgMAF_RGHw", Guid "b1754c14-d296-4b0f-a09a-030017f4461f" 21 | "Xy0MVKupFES9NpmZ9TiHcw", Guid "540c2d5f-a9ab-4414-bd36-9999f5388773" 22 | ] 23 | 24 | testCases 25 | |> List.iter (fun (shortGuid, expectedGuid) -> 26 | let guid = ShortGuid.toGuid shortGuid 27 | Assert.Equal(expectedGuid, guid) |> ignore 28 | ) 29 | 30 | [] 31 | let ``Long Guids translate to correct short Guids`` () = 32 | let testCases = 33 | [ 34 | "FEx1sZbSD0ugmgMAF_RGHw", Guid "b1754c14-d296-4b0f-a09a-030017f4461f" 35 | "Xy0MVKupFES9NpmZ9TiHcw", Guid "540c2d5f-a9ab-4414-bd36-9999f5388773" 36 | ] 37 | 38 | testCases 39 | |> List.iter (fun (shortGuid, longGuid) -> 40 | let guid = ShortGuid.fromGuid longGuid 41 | Assert.Equal(shortGuid, guid) |> ignore 42 | ) 43 | 44 | [] 45 | let ``Short Guids are always 22 characters long`` () = 46 | let testCases = [ 0..10 ] |> List.map (fun _ -> Guid.NewGuid()) 47 | 48 | testCases 49 | |> List.iter (fun guid -> 50 | let shortGuid = ShortGuid.fromGuid guid 51 | Assert.Equal(22, shortGuid.Length) |> ignore 52 | ) 53 | 54 | [] 55 | let ``Short Ids are always 11 characters long`` () = 56 | let rand = new Random() 57 | let testCases = [ 0..10 ] |> List.map (fun _ -> rndInt64 rand) 58 | 59 | testCases 60 | |> List.iter (fun id -> 61 | let shortId = ShortId.fromUInt64 id 62 | Assert.Equal(11, shortId.Length) |> ignore 63 | ) 64 | 65 | [] 66 | let ``Short Ids translate correctly back and forth`` () = 67 | let rand = new Random() 68 | let testCases = [ 0..10 ] |> List.map (fun _ -> rndInt64 rand) 69 | 70 | testCases 71 | |> List.iter (fun origId -> 72 | let shortId = ShortId.fromUInt64 origId 73 | let id = ShortId.toUInt64 shortId 74 | Assert.Equal(origId, id) |> ignore 75 | ) 76 | 77 | [] 78 | let ``Short Ids translate to correct uint64 values`` () = 79 | let testCases = 80 | [ 81 | "r1iKapqh_s4", 12635000945053400782UL 82 | "5aLu720NzTs", 16547050693006839099UL 83 | "BdQ5vc0d8-I", 420024152605193186UL 84 | "FOwfPLe6waQ", 1507614320903242148UL 85 | ] 86 | 87 | testCases 88 | |> List.iter (fun (shortId, id) -> 89 | let result = ShortId.toUInt64 shortId 90 | Assert.Equal(id, result) |> ignore 91 | ) 92 | 93 | [] 94 | let ``UInt64 values translate to correct short IDs`` () = 95 | let testCases = 96 | [ 97 | "r1iKapqh_s4", 12635000945053400782UL 98 | "5aLu720NzTs", 16547050693006839099UL 99 | "BdQ5vc0d8-I", 420024152605193186UL 100 | "FOwfPLe6waQ", 1507614320903242148UL 101 | ] 102 | 103 | testCases 104 | |> List.iter (fun (shortId, id) -> 105 | let result = ShortId.fromUInt64 id 106 | Assert.Equal(shortId, result) |> ignore 107 | ) 108 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at giraffe@dusted.codes. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /tests/Giraffe.Tests/ModelValidationTests.fs: -------------------------------------------------------------------------------- 1 | module Giraffe.Tests.ModelValidationTests 2 | 3 | open System 4 | open System.Net 5 | open System.Net.Http 6 | open Microsoft.AspNetCore.Builder 7 | open Microsoft.Extensions.DependencyInjection 8 | open Microsoft.Extensions.Logging 9 | open Xunit 10 | open Giraffe 11 | 12 | // --------------------------------- 13 | // Model Validation App 14 | // --------------------------------- 15 | 16 | [] 17 | type Adult = 18 | { 19 | FirstName: string 20 | MiddleName: string option 21 | LastName: string 22 | Age: int 23 | } 24 | 25 | override this.ToString() = 26 | sprintf 27 | "Name: %s%s %s, Age: %i" 28 | this.FirstName 29 | (if this.MiddleName.IsSome then 30 | " " + this.MiddleName.Value 31 | else 32 | "") 33 | this.LastName 34 | this.Age 35 | 36 | member this.HasErrors() = 37 | if this.FirstName.Length < 3 then 38 | Some "First name is too short." 39 | else if this.FirstName.Length > 50 then 40 | Some "First name is too long." 41 | else if this.LastName.Length < 3 then 42 | Some "Last name is too short." 43 | else if this.LastName.Length > 50 then 44 | Some "Last name is too long." 45 | else if this.Age < 18 then 46 | Some "Person must be an adult (age >= 18)." 47 | else if this.Age > 150 then 48 | Some "Person must be a human being." 49 | else 50 | None 51 | 52 | interface IModelValidation with 53 | member this.Validate() = 54 | match this.HasErrors() with 55 | | Some msg -> Error(RequestErrors.badRequest (text msg)) 56 | | None -> Ok this 57 | 58 | module Urls = 59 | let person = "/person" 60 | 61 | module WebApp = 62 | let textHandler (x: obj) = text (x.ToString()) 63 | let parsingErrorHandler err = RequestErrors.badRequest (text err) 64 | let culture = None 65 | let tryBindQueryToAdult = tryBindQuery parsingErrorHandler culture 66 | 67 | let webApp _ = 68 | choose [ route Urls.person >=> tryBindQueryToAdult (validateModel textHandler) ] 69 | 70 | let errorHandler (ex: Exception) (_: ILogger) : HttpHandler = 71 | printfn "Error: %s" ex.Message 72 | printfn "StackTrace:%s %s" Environment.NewLine ex.StackTrace 73 | setStatusCode 500 >=> text ex.Message 74 | 75 | let configureApp args (app: IApplicationBuilder) = 76 | app.UseGiraffeErrorHandler(errorHandler).UseGiraffe(webApp args) 77 | 78 | let configureServices (services: IServiceCollection) = services.AddGiraffe() |> ignore 79 | 80 | let makeRequest req = 81 | makeRequest WebApp.configureApp WebApp.configureServices req 82 | 83 | // --------------------------------- 84 | // Tests 85 | // --------------------------------- 86 | 87 | [] 88 | let ``validateModel with valid model`` () = 89 | task { 90 | let url = sprintf "%s?firstName=John&lastName=Doe&age=35" Urls.person 91 | let! response = createRequest HttpMethod.Get url |> makeRequest (None, None) 92 | let! content = response |> isStatus HttpStatusCode.OK |> readText 93 | content |> shouldEqual "Name: John Doe, Age: 35" 94 | } 95 | 96 | [] 97 | let ``validateModel with invalid model`` () = 98 | task { 99 | let url = sprintf "%s?firstName=John&lastName=Doe&age=17" Urls.person 100 | let! response = createRequest HttpMethod.Get url |> makeRequest (None, None) 101 | let! content = response |> isStatus HttpStatusCode.BadRequest |> readText 102 | content |> shouldEqual "Person must be an adult (age >= 18)." 103 | } 104 | -------------------------------------------------------------------------------- /src/Giraffe/HttpStatusCodeHandlers.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Giraffe.HttpStatusCodeHandlers 3 | 4 | /// 5 | /// A collection of functions to return HTTP status code 1xx responses. 6 | /// 7 | module Intermediate = 8 | let CONTINUE: HttpHandler = setStatusCode 100 >=> setBody [||] 9 | let SWITCHING_PROTO: HttpHandler = setStatusCode 101 >=> setBody [||] 10 | 11 | /// 12 | /// A collection of functions to return HTTP status code 2xx responses. 13 | /// 14 | module Successful = 15 | let ok x = setStatusCode 200 >=> x 16 | let OK x = ok (negotiate x) 17 | 18 | let created x = setStatusCode 201 >=> x 19 | let CREATED x = created (negotiate x) 20 | 21 | let accepted x = setStatusCode 202 >=> x 22 | let ACCEPTED x = accepted (negotiate x) 23 | 24 | let NO_CONTENT: HttpHandler = setStatusCode 204 25 | 26 | /// 27 | /// A collection of functions to return HTTP status code 4xx responses. 28 | /// 29 | module RequestErrors = 30 | let badRequest x = setStatusCode 400 >=> x 31 | let BAD_REQUEST x = badRequest (negotiate x) 32 | 33 | /// 34 | /// Sends a 401 Unauthorized HTTP status code response back to the client. 35 | /// 36 | /// Use the unauthorized status code handler when a user could not be authenticated by the server (either missing or wrong authentication data). By returning a 401 Unauthorized HTTP response the server tells the client that it must know who is making the request before it can return a successful response. As such the server must also include which authentication scheme the client must use in order to successfully authenticate. 37 | /// 38 | /// 39 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate 40 | /// http://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses/12675357 41 | /// 42 | /// List of authentication schemes: 43 | /// 44 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#Authentication_schemes 45 | /// 46 | let unauthorized scheme realm x = 47 | setStatusCode 401 48 | >=> setHttpHeader "WWW-Authenticate" (sprintf "%s realm=\"%s\"" scheme realm) 49 | >=> x 50 | 51 | let UNAUTHORIZED scheme realm x = unauthorized scheme realm (negotiate x) 52 | 53 | let forbidden x = setStatusCode 403 >=> x 54 | let FORBIDDEN x = forbidden (negotiate x) 55 | 56 | let notFound x = setStatusCode 404 >=> x 57 | let NOT_FOUND x = notFound (negotiate x) 58 | 59 | let methodNotAllowed x = setStatusCode 405 >=> x 60 | let METHOD_NOT_ALLOWED x = methodNotAllowed (negotiate x) 61 | 62 | let notAcceptable x = setStatusCode 406 >=> x 63 | let NOT_ACCEPTABLE x = notAcceptable (negotiate x) 64 | 65 | let conflict x = setStatusCode 409 >=> x 66 | let CONFLICT x = conflict (negotiate x) 67 | 68 | let gone x = setStatusCode 410 >=> x 69 | let GONE x = gone (negotiate x) 70 | 71 | let unsupportedMediaType x = setStatusCode 415 >=> x 72 | let UNSUPPORTED_MEDIA_TYPE x = unsupportedMediaType (negotiate x) 73 | 74 | let unprocessableEntity x = setStatusCode 422 >=> x 75 | let UNPROCESSABLE_ENTITY x = unprocessableEntity (negotiate x) 76 | 77 | let preconditionRequired x = setStatusCode 428 >=> x 78 | let PRECONDITION_REQUIRED x = preconditionRequired (negotiate x) 79 | 80 | let tooManyRequests x = setStatusCode 429 >=> x 81 | let TOO_MANY_REQUESTS x = tooManyRequests (negotiate x) 82 | 83 | 84 | /// 85 | /// A collection of functions to return HTTP status code 5xx responses. 86 | /// 87 | module ServerErrors = 88 | let internalError x = setStatusCode 500 >=> x 89 | let INTERNAL_ERROR x = internalError (negotiate x) 90 | 91 | let notImplemented x = setStatusCode 501 >=> x 92 | let NOT_IMPLEMENTED x = notImplemented (negotiate x) 93 | 94 | let badGateway x = setStatusCode 502 >=> x 95 | let BAD_GATEWAY x = badGateway (negotiate x) 96 | 97 | let serviceUnavailable x = setStatusCode 503 >=> x 98 | let SERVICE_UNAVAILABLE x = serviceUnavailable (negotiate x) 99 | 100 | let gatewayTimeout x = setStatusCode 504 >=> x 101 | let GATEWAY_TIMEOUT x = gatewayTimeout (negotiate x) 102 | 103 | let invalidHttpVersion x = setStatusCode 505 >=> x 104 | -------------------------------------------------------------------------------- /src/Giraffe/ShortGuid.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe 2 | 3 | /// 4 | /// Short GUIDs are a shorter, URL-friendlier version 5 | /// of the traditional type. 6 | /// 7 | /// Short GUIDs are always 22 characters long, which let's 8 | /// one save a total of 10 characters in comparison to using 9 | /// a normal as identifier. 10 | /// 11 | /// Additionally a Short GUID is by default a URL encoded 12 | /// string which doesn't need extra character replacing 13 | /// before using of it in a URL query parameter. 14 | /// 15 | /// All Short GUID strings map directly to a 16 | /// object and the `ShortGuid` module can be used to convert 17 | /// a into a short GUID and vice versa. 18 | /// 19 | /// For more information please check: 20 | /// https://madskristensen.net/blog/A-shorter-and-URL-friendly-GUID 21 | /// 22 | [] 23 | module ShortGuid = 24 | open System 25 | 26 | /// 27 | /// Converts a into a 22 character long 28 | /// short GUID string. 29 | /// 30 | /// The to be converted into a short GUID. 31 | /// Returns a 22 character long URL encoded short GUID string. 32 | let fromGuid (guid: Guid) = 33 | guid.ToByteArray() 34 | |> Convert.ToBase64String 35 | |> (fun str -> str.Replace("/", "_").Replace("+", "-").Substring(0, 22)) 36 | 37 | /// 38 | /// Converts a 22 character short GUID string into the matching . 39 | /// 40 | /// The short GUID string to be converted into a . 41 | /// Returns a object. 42 | let toGuid (shortGuid: string) = 43 | shortGuid.Replace("_", "/").Replace("-", "+") 44 | |> (fun str -> str + "==") 45 | |> Convert.FromBase64String 46 | |> Guid 47 | 48 | /// 49 | /// Short IDs are a shorter, URL-friendlier version 50 | /// of an unsigned 64-bit integer value (`uint64` in F# and `ulong` in C#). 51 | /// 52 | /// Short IDs are always 11 characters long, which let's 53 | /// one save a total of 9 characters in comparison to using 54 | /// a normal `uint64` value as identifier. 55 | /// 56 | /// Additionally a Short ID is by default a URL encoded 57 | /// string which doesn't need extra character replacing 58 | /// before using it in a URL query parameter. 59 | /// 60 | /// All Short ID strings map directly to a `uint64` object 61 | /// and the `ShortId` module can be used to convert an 62 | /// `uint64` value into a short ID `string` and vice versa. 63 | /// 64 | /// For more information please check: 65 | /// https://webapps.stackexchange.com/questions/54443/format-for-id-of-youtube-video 66 | /// 67 | [] 68 | module ShortId = 69 | open System 70 | 71 | /// 72 | /// Converts a uint64 value into a 11 character long 73 | /// short ID string. 74 | /// 75 | /// The uint64 to be converted into a short ID. 76 | /// Returns a 11 character long URL encoded short ID string. 77 | let fromUInt64 (id: uint64) = 78 | BitConverter.GetBytes id 79 | |> (fun arr -> 80 | match BitConverter.IsLittleEndian with 81 | | true -> 82 | Array.Reverse arr 83 | arr 84 | | false -> arr 85 | ) 86 | |> Convert.ToBase64String 87 | |> (fun str -> str.Remove(11, 1).Replace("/", "_").Replace("+", "-")) 88 | 89 | /// 90 | /// Converts a 11 character short ID string into the matching uint64 value. 91 | /// 92 | /// The short ID string to be converted into a uint64 value. 93 | /// The short ID string to be converted into a uint64 value. 94 | let toUInt64 (shortId: string) = 95 | let bytes = 96 | shortId.Replace("_", "/").Replace("-", "+") 97 | |> (fun str -> str + "=") 98 | |> Convert.FromBase64String 99 | |> (fun arr -> 100 | match BitConverter.IsLittleEndian with 101 | | true -> 102 | Array.Reverse arr 103 | arr 104 | | false -> arr 105 | ) 106 | 107 | BitConverter.ToUInt64(bytes, 0) 108 | -------------------------------------------------------------------------------- /samples/NewtonsoftJson/Program.fs: -------------------------------------------------------------------------------- 1 | open Microsoft.AspNetCore.Builder 2 | open Microsoft.AspNetCore.Http 3 | open Microsoft.Extensions.DependencyInjection 4 | open Microsoft.Extensions.Hosting 5 | open Giraffe 6 | open Giraffe.EndpointRouting 7 | open Newtonsoft.Json 8 | 9 | [] 10 | module NewtonsoftJson = 11 | open System.IO 12 | open System.Text 13 | open System.Threading.Tasks 14 | open Microsoft.IO 15 | open Newtonsoft.Json.Serialization 16 | 17 | type Serializer(settings: JsonSerializerSettings, rmsManager: RecyclableMemoryStreamManager) = 18 | let serializer = JsonSerializer.Create settings 19 | let utf8EncodingWithoutBom = UTF8Encoding(false) 20 | 21 | static member DefaultSettings = 22 | JsonSerializerSettings(ContractResolver = CamelCasePropertyNamesContractResolver()) 23 | 24 | interface Json.ISerializer with 25 | member __.SerializeToString(x: 'T) = 26 | JsonConvert.SerializeObject(x, settings) 27 | 28 | member __.SerializeToBytes(x: 'T) = 29 | JsonConvert.SerializeObject(x, settings) |> Encoding.UTF8.GetBytes 30 | 31 | member __.SerializeToStreamAsync (x: 'T) (stream: Stream) = 32 | task { 33 | use memoryStream = rmsManager.GetStream("giraffe-json-serialize-to-stream") 34 | use streamWriter = new StreamWriter(memoryStream, utf8EncodingWithoutBom) 35 | use jsonTextWriter = new JsonTextWriter(streamWriter) 36 | serializer.Serialize(jsonTextWriter, x) 37 | jsonTextWriter.Flush() 38 | memoryStream.Seek(0L, SeekOrigin.Begin) |> ignore 39 | do! memoryStream.CopyToAsync(stream, 65536) 40 | } 41 | :> Task 42 | 43 | member __.Deserialize<'T>(json: string) : 'T = 44 | match JsonConvert.DeserializeObject<'T>(json, settings) with 45 | | null -> failwith "Deserialized object is null" 46 | | (notNull: 'T) -> notNull 47 | 48 | member __.Deserialize<'T>(bytes: byte array) : 'T = 49 | let json = Encoding.UTF8.GetString bytes 50 | 51 | match JsonConvert.DeserializeObject<'T>(json, settings) with 52 | | null -> failwith "Deserialized object is null" 53 | | (notNull: 'T) -> notNull 54 | 55 | member __.DeserializeAsync<'T>(stream: Stream) : Task<'T> = 56 | task { 57 | use memoryStream = rmsManager.GetStream("giraffe-json-deserialize") 58 | do! stream.CopyToAsync(memoryStream) 59 | memoryStream.Seek(0L, SeekOrigin.Begin) |> ignore 60 | use streamReader = new StreamReader(memoryStream) 61 | use jsonTextReader = new JsonTextReader(streamReader) 62 | 63 | return 64 | match serializer.Deserialize<'T>(jsonTextReader) with 65 | | null -> failwith "Deserialized object is null" 66 | | (notNull: 'T) -> notNull 67 | } 68 | 69 | type JsonResponse = { Foo: string; Bar: string; Age: int } 70 | 71 | let endpoints: Endpoint list = 72 | [ GET [ route "/json" (json { Foo = "john"; Bar = "doe"; Age = 30 }) ] ] 73 | 74 | let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound 75 | 76 | let configureServices (services: IServiceCollection) = 77 | services 78 | .AddSingleton(fun serviceProvider -> 79 | let rmsManager = 80 | serviceProvider.GetService() 81 | |> function 82 | | null -> Microsoft.IO.RecyclableMemoryStreamManager() 83 | | notNull -> notNull 84 | 85 | NewtonsoftJson.Serializer(JsonSerializerSettings(), rmsManager) :> Json.ISerializer 86 | ) 87 | .AddRouting() 88 | .AddResponseCaching() 89 | .AddGiraffe() 90 | |> ignore 91 | 92 | let configureApp (appBuilder: IApplicationBuilder) = 93 | appBuilder.UseRouting().UseResponseCaching().UseEndpoints(_.MapGiraffeEndpoints(endpoints)).UseGiraffe 94 | notFoundHandler 95 | 96 | [] 97 | let main (args: string array) : int = 98 | let builder = WebApplication.CreateBuilder(args) 99 | configureServices builder.Services 100 | 101 | let app = builder.Build() 102 | 103 | configureApp app 104 | app.Run() 105 | 106 | 0 107 | -------------------------------------------------------------------------------- /src/Giraffe/Giraffe.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Giraffe 5 | 8.2.0 6 | A native functional ASP.NET Core web framework for F# developers. 7 | Copyright 2020 Dustin Moris Gorski 8 | Dustin Moris Gorski and contributors 9 | en-GB 10 | 11 | 12 | net6.0;net7.0;net8.0;net9.0 13 | portable 14 | Library 15 | true 16 | false 17 | true 18 | true 19 | true 20 | 21 | 22 | Giraffe 23 | Giraffe;ASP.NET Core;Lambda;FSharp;Functional;Http;Web;Framework;Micro;Service 24 | https://raw.githubusercontent.com/giraffe-fsharp/giraffe/master/RELEASE_NOTES.md 25 | https://github.com/giraffe-fsharp/giraffe 26 | giraffe-64x64.png 27 | true 28 | git 29 | https://github.com/giraffe-fsharp/Giraffe 30 | 31 | LICENSE 32 | README.md 33 | 34 | true 35 | true 36 | 37 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 38 | FS2003;FS0044 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | all 55 | build 56 | 57 | 58 | all 59 | analyzers 60 | 61 | 62 | all 63 | analyzers 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/Giraffe/ResponseCaching.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe 2 | 3 | [] 4 | module ResponseCaching = 5 | open System 6 | open Microsoft.Extensions.Primitives 7 | open Microsoft.Net.Http.Headers 8 | open Microsoft.AspNetCore.Http 9 | open Microsoft.AspNetCore.ResponseCaching 10 | 11 | /// 12 | /// Specifies the directive for the `Cache-Control` HTTP header: 13 | /// 14 | /// NoCache: The resource should not be cached under any circumstances. 15 | /// Public: Any client and proxy may cache the resource for the given amount of time. 16 | /// Private: Only the end client may cache the resource for the given amount of time. 17 | /// 18 | type CacheDirective = 19 | | NoCache 20 | | Public of TimeSpan 21 | | Private of TimeSpan 22 | 23 | let private noCacheHeader = CacheControlHeaderValue(NoCache = true, NoStore = true) 24 | 25 | let inline private cacheHeader isPublic duration = 26 | CacheControlHeaderValue(Public = isPublic, MaxAge = Nullable duration) 27 | 28 | /// 29 | /// Enables (or suppresses) response caching by clients and proxy servers. 30 | /// This http handler integrates with ASP.NET Core's response caching middleware. 31 | /// 32 | /// The responseCaching http handler will set the relevant HTTP response headers in order to enable response caching on the client, by proxies (if public) and by the ASP.NET Core middleware (if enabled). 33 | /// 34 | /// Specifies the cache directive to be set in the response's HTTP headers. Use NoCache to suppress caching altogether or use Public`/`Private to enable caching for everyone or clients only. 35 | /// Optionally specify which HTTP headers have to match in order to return a cached response (e.g. Accept and/or Accept-Encoding). 36 | /// An optional list of query keys which will be used by the ASP.NET Core response caching middleware to vary (potentially) cached responses. If this parameter is used then the ASP.NET Core response caching middleware has to be enabled. For more information check the official [VaryByQueryKeys](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/middleware?view=aspnetcore-2.1#varybyquerykeys) documentation. 37 | /// 38 | /// 39 | /// A Giraffe function which can be composed into a bigger web application. 40 | let responseCaching 41 | (directive: CacheDirective) 42 | (vary: string option) 43 | (varyByQueryKeys: string array option) 44 | : HttpHandler = 45 | fun (next: HttpFunc) (ctx: HttpContext) -> 46 | 47 | let tHeaders = ctx.Response.GetTypedHeaders() 48 | let headers = ctx.Response.Headers 49 | 50 | match directive with 51 | | NoCache -> 52 | tHeaders.CacheControl <- noCacheHeader 53 | headers.[HeaderNames.Pragma] <- StringValues [| "no-cache" |] 54 | headers.[HeaderNames.Expires] <- StringValues [| "-1" |] 55 | | Public duration -> tHeaders.CacheControl <- cacheHeader true duration 56 | | Private duration -> tHeaders.CacheControl <- cacheHeader false duration 57 | 58 | vary 59 | |> Option.iter (fun value -> headers.[HeaderNames.Vary] <- StringValues [| value |]) 60 | 61 | varyByQueryKeys 62 | |> Option.iter (fun value -> 63 | let responseCachingFeature = ctx.Features.Get() 64 | 65 | if isNotNull responseCachingFeature then 66 | responseCachingFeature.VaryByQueryKeys <- value 67 | ) 68 | 69 | next ctx 70 | 71 | /// 72 | /// Disables response caching by clients and proxy servers. 73 | /// 74 | /// A Giraffe `HttpHandler` function which can be composed into a bigger web application. 75 | let noResponseCaching: HttpHandler = responseCaching NoCache None None 76 | 77 | /// 78 | /// Enables response caching for clients only. 79 | /// 80 | /// The http handler will set the relevant HTTP response headers in order to enable response caching on the client only. 81 | /// 82 | /// Specifies the duration (in seconds) for which the response may be cached. 83 | /// Optionally specify which HTTP headers have to match in order to return a cached response (e.g. Accept and/or Accept-Encoding). 84 | /// A Giraffe function which can be composed into a bigger web application. 85 | let privateResponseCaching (seconds: int) (vary: string option) : HttpHandler = 86 | responseCaching (Private(TimeSpan.FromSeconds(float seconds))) vary None 87 | 88 | /// 89 | /// Enables response caching for clients and proxy servers. 90 | /// This http handler integrates with ASP.NET Core's response caching middleware. 91 | /// 92 | /// The http handler will set the relevant HTTP response headers in order to enable response caching on the client, by proxies and by the ASP.NET Core middleware (if enabled). 93 | /// 94 | /// 95 | /// Specifies the duration (in seconds) for which the response may be cached. 96 | /// Optionally specify which HTTP headers have to match in order to return a cached response (e.g. `Accept` and/or `Accept-Encoding`). 97 | /// A Giraffe function which can be composed into a bigger web application. 98 | let publicResponseCaching (seconds: int) (vary: string option) : HttpHandler = 99 | responseCaching (Public(TimeSpan.FromSeconds(float seconds))) vary None 100 | -------------------------------------------------------------------------------- /tests/Giraffe.Tests/XmlTests.fs: -------------------------------------------------------------------------------- 1 | module Giraffe.Tests.XmlTests 2 | 3 | open System 4 | open System.IO 5 | open System.Text 6 | open System.Net.Http 7 | open System.Collections.Generic 8 | open System.Threading.Tasks 9 | open Microsoft.AspNetCore.Http 10 | open Microsoft.AspNetCore.Builder 11 | open Microsoft.Extensions.Primitives 12 | open Microsoft.Extensions.DependencyInjection 13 | open Xunit 14 | open NSubstitute 15 | open Giraffe 16 | 17 | // --------------------------------- 18 | // XML Tests 19 | // --------------------------------- 20 | 21 | [] 22 | type MyXmlRecord = 23 | { 24 | Foo: string 25 | Bar: string 26 | Baz: float 27 | } 28 | 29 | static member Empty = { Foo = null; Bar = null; Baz = 0.0 } 30 | 31 | let private dictForDefaultSerializer = 32 | [ 33 | { 34 | Foo = "hello" 35 | Bar = "world" 36 | Baz = 12.5 37 | } 38 | { 39 | Foo = "hello" 40 | Bar = null 41 | Baz = 12.5 42 | } 43 | { 44 | Foo = "hello" 45 | Bar = null 46 | Baz = 12.5 47 | } 48 | { 49 | Foo = "hello" 50 | Bar = "world" 51 | Baz = 0.0 52 | } 53 | { Foo = "hello"; Bar = null; Baz = 0.0 } 54 | { Foo = null; Bar = null; Baz = 0.0 } 55 | { Foo = null; Bar = null; Baz = 0.0 } 56 | ] 57 | |> List.mapi (fun i r -> i, r) 58 | |> dict 59 | 60 | let private xmlParserHandler (expected: MyXmlRecord) : HttpHandler = 61 | fun next (ctx: HttpContext) -> 62 | task { 63 | try 64 | let! model = ctx.BindXmlAsync() 65 | 66 | match model with 67 | | m when m.Foo = expected.Foo && m.Bar = expected.Bar && m.Baz = expected.Baz -> 68 | return! (setStatusCode 201 >=> xml model) next ctx 69 | | _ -> 70 | return! 71 | (setStatusCode 400 72 | >=> xml 73 | {| 74 | Error = "Expected is different from actual" 75 | |}) 76 | next 77 | ctx 78 | with ex -> 79 | return! (setStatusCode 500 >=> xml {| Error = ex.Message |}) next ctx 80 | } 81 | 82 | // ---------------------------------------------------------- 83 | // Using the default XML serializer with Giraffe's internal routing mechanism 84 | 85 | [] 86 | [helloworld12.5""", 0)>] 87 | [hello12.5""", 1)>] 88 | [hello12.5""", 2)>] 89 | [helloworld0""", 3)>] 90 | [hello0""", 4)>] 91 | [0""", 5)>] 92 | [0""", 6)>] 93 | let ``xml parsing works properly`` (reqBody: string, expectedDictKey: int) = 94 | task { 95 | let expected = dictForDefaultSerializer.[expectedDictKey] 96 | 97 | let app = choose [ POST >=> route "/parse-xml" >=> xmlParserHandler expected ] 98 | 99 | let ctx = Substitute.For() 100 | mockXml ctx 101 | 102 | let stream = new MemoryStream() 103 | let writer = new StreamWriter(stream, Text.Encoding.UTF8) 104 | writer.Write reqBody 105 | writer.Flush() 106 | stream.Position <- 0L 107 | 108 | ctx.Request.Method.ReturnsForAnyArgs "POST" |> ignore 109 | ctx.Request.Path.ReturnsForAnyArgs(PathString "/parse-xml") |> ignore 110 | ctx.Response.Body <- new MemoryStream() 111 | ctx.Request.Body <- stream 112 | 113 | let! result = app next ctx 114 | 115 | Assert.Equal(true, result.IsSome) 116 | Assert.Equal(201, ctx.Response.StatusCode) 117 | } 118 | 119 | // ---------------------------------------------------------- 120 | // Using the default XML serializer with endpoint routing 121 | 122 | open Giraffe.EndpointRouting 123 | 124 | [] 125 | [helloworld12.5""", 0)>] 126 | [hello12.5""", 1)>] 127 | [hello12.5""", 2)>] 128 | [helloworld0""", 3)>] 129 | [hello0""", 4)>] 130 | [0""", 5)>] 131 | [0""", 6)>] 132 | let ``xml parsing works properly with endpoint routing`` (reqBody: string, expectedDictKey: int) = 133 | task { 134 | let expected = dictForDefaultSerializer.[expectedDictKey] 135 | 136 | let endpoints: Endpoint list = 137 | [ POST [ route "/xml-parser" (xmlParserHandler expected) ] ] 138 | 139 | let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound 140 | 141 | let configureApp (app: IApplicationBuilder) = 142 | app.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler) 143 | 144 | let configureServices (services: IServiceCollection) = 145 | services.AddRouting().AddGiraffe() |> ignore 146 | 147 | let request = createRequest HttpMethod.Post "/xml-parser" 148 | request.Content <- new StringContent(reqBody, Encoding.UTF8, "application/xml") 149 | 150 | let! response = makeRequest (fun () -> configureApp) configureServices () request 151 | 152 | Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode) 153 | } 154 | -------------------------------------------------------------------------------- /src/Giraffe/Middleware.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Giraffe.Middleware 3 | 4 | open System 5 | open System.Runtime.CompilerServices 6 | open System.Threading.Tasks 7 | open Microsoft.AspNetCore.Builder 8 | open Microsoft.AspNetCore.Http 9 | open Microsoft.Extensions.Logging 10 | open Microsoft.Extensions.DependencyInjection 11 | open Microsoft.Extensions.DependencyInjection.Extensions 12 | open Microsoft.IO 13 | 14 | // --------------------------- 15 | // Default middleware 16 | // --------------------------- 17 | 18 | type GiraffeMiddleware(next: RequestDelegate, handler: HttpHandler, loggerFactory: ILoggerFactory) = 19 | 20 | do 21 | if isNull next then 22 | raise (ArgumentNullException("next")) 23 | 24 | let logger = loggerFactory.CreateLogger() 25 | 26 | // pre-compile the handler pipeline 27 | let func: HttpFunc = handler earlyReturn 28 | 29 | member __.Invoke(ctx: HttpContext) = 30 | task { 31 | let start = System.Diagnostics.Stopwatch.GetTimestamp() 32 | 33 | let! result = func ctx 34 | 35 | if logger.IsEnabled LogLevel.Debug then 36 | let freq = double System.Diagnostics.Stopwatch.Frequency 37 | let stop = System.Diagnostics.Stopwatch.GetTimestamp() 38 | let elapsedMs = (double (stop - start)) * 1000.0 / freq 39 | 40 | logger.LogDebug( 41 | "Giraffe returned {SomeNoneResult} for {HttpProtocol} {HttpMethod} at {Path} in {ElapsedMs}", 42 | (if result.IsSome then "Some" else "None"), 43 | ctx.Request.Protocol, 44 | ctx.Request.Method, 45 | ctx.Request.Path.ToString(), 46 | elapsedMs 47 | ) 48 | 49 | if (result.IsNone) then 50 | return! next.Invoke ctx 51 | } 52 | 53 | // --------------------------- 54 | // Error Handling middleware 55 | // --------------------------- 56 | 57 | type GiraffeErrorHandlerMiddleware(next: RequestDelegate, errorHandler: ErrorHandler, loggerFactory: ILoggerFactory) = 58 | 59 | do 60 | if isNull next then 61 | raise (ArgumentNullException("next")) 62 | 63 | member __.Invoke(ctx: HttpContext) = 64 | task { 65 | try 66 | return! next.Invoke ctx 67 | with ex -> 68 | let logger = loggerFactory.CreateLogger() 69 | 70 | try 71 | let func = (Some >> Task.FromResult) 72 | let! _ = errorHandler ex logger func ctx 73 | return () 74 | with ex2 -> 75 | logger.LogError(EventId(0), ex, "An unhandled exception has occurred while executing the request.") 76 | 77 | logger.LogError( 78 | EventId(0), 79 | ex2, 80 | "An exception was thrown attempting to handle the original exception." 81 | ) 82 | } 83 | 84 | // --------------------------- 85 | // Extension methods for convenience 86 | // --------------------------- 87 | 88 | [] 89 | type ApplicationBuilderExtensions() = 90 | /// 91 | /// Adds the into the ASP.NET Core pipeline. Any web request which doesn't get handled by a surrounding middleware can be picked up by the Giraffe pipeline. 92 | /// 93 | /// It is generally recommended to add the after the error handling, static file and any authentication middleware. 94 | /// 95 | /// The ASP.NET Core application builder. 96 | /// The Giraffe pipeline. The handler can be anything from a single handler to an entire web application which has been composed from many smaller handlers. 97 | /// 98 | [] 99 | static member UseGiraffe(builder: IApplicationBuilder, handler: HttpHandler) = 100 | builder.UseMiddleware handler |> ignore 101 | 102 | /// 103 | /// Adds the into the ASP.NET Core pipeline. The has been configured in such a way that it only invokes the when an unhandled exception bubbles up to the middleware. It therefore is recommended to add the as the very first middleware above everything else. 104 | /// 105 | /// The ASP.NET Core application builder. 106 | /// The Giraffe pipeline. The handler can be anything from a single handler to a bigger error application which has been composed from many smaller handlers. 107 | /// Returns an builder object. 108 | [] 109 | static member UseGiraffeErrorHandler(builder: IApplicationBuilder, handler: ErrorHandler) = 110 | builder.UseMiddleware handler 111 | 112 | [] 113 | type ServiceCollectionExtensions() = 114 | /// 115 | /// Adds default Giraffe services to the ASP.NET Core service container. 116 | /// 117 | /// The default services include features like , , or more. Please check the official Giraffe documentation for an up to date list of configurable services. 118 | /// 119 | /// Returns an builder object. 120 | [] 121 | static member AddGiraffe(svc: IServiceCollection) = 122 | svc.TryAddSingleton(fun _ -> RecyclableMemoryStreamManager()) 123 | 124 | svc.TryAddSingleton(fun _ -> 125 | Json.Serializer(Json.Serializer.DefaultOptions) :> Json.ISerializer 126 | ) 127 | 128 | svc.TryAddSingleton(fun sp -> 129 | SystemXml.Serializer(SystemXml.Serializer.DefaultSettings, sp.GetService()) 130 | :> Xml.ISerializer 131 | ) 132 | 133 | svc.TryAddSingleton() 134 | svc 135 | -------------------------------------------------------------------------------- /samples/ResponseCachingApp/README.md: -------------------------------------------------------------------------------- 1 | # Response Caching App 2 | 3 | The purpose of this sample is to show how one can configure the Giraffe server to use the ASP.NET [response caching](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-7.0) feature. Notice that we're leveraging the middlewares which are offered by Giraffe. 4 | 5 | You can find their documentation here: [Giraffe Docs - Response Caching](https://giraffe.wiki/docs#response-caching). 6 | 7 | ## How to test 8 | 9 | First, start the server at the terminal using: 10 | 11 | ```bash 12 | # Assuming that you're at the top level of this repository 13 | dotnet run --project samples/ResponseCachingApp/ 14 | 15 | # It will start the server listening to port 5000 16 | ``` 17 | 18 | Now, you can use the `test-run.fsx` script: 19 | 20 | ```bash 21 | dotnet fsi samples/ResponseCachingApp/test-run.fsx 22 | ``` 23 | 24 | And the expected result: 25 | 26 | ```bash 27 | # ----------------------------------- 28 | # Testing the /cached/not endpoint 29 | 30 | # Sending request GET http://localhost:5000/cached/not ... 31 | # 200 (OK) (GET http://localhost:5000/cached/not) 32 | # Hello World -> DateTime: 8/30/2023 5:06:00 PM 33 | 34 | # Sending request GET http://localhost:5000/cached/not ... 35 | # 200 (OK) (GET http://localhost:5000/cached/not) 36 | # Hello World -> DateTime: 8/30/2023 5:06:06 PM 37 | 38 | # Sending request GET http://localhost:5000/cached/not ... 39 | # 200 (OK) (GET http://localhost:5000/cached/not) 40 | # Hello World -> DateTime: 8/30/2023 5:06:12 PM 41 | 42 | # Sending request GET http://localhost:5000/cached/not ... 43 | # 200 (OK) (GET http://localhost:5000/cached/not) 44 | # Hello World -> DateTime: 8/30/2023 5:06:18 PM 45 | 46 | # Sending request GET http://localhost:5000/cached/not ... 47 | # 200 (OK) (GET http://localhost:5000/cached/not) 48 | # Hello World -> DateTime: 8/30/2023 5:06:24 PM 49 | 50 | # The time it took to finish: 51 | # 30.47 seconds 52 | 53 | # ----------------------------------- 54 | # Testing the /cached/public endpoint 55 | 56 | # Sending request GET http://localhost:5000/cached/public ... 57 | # 200 (OK) (GET http://localhost:5000/cached/public) 58 | # Hello World -> DateTime: 8/30/2023 5:06:30 PM 59 | 60 | # Sending request GET http://localhost:5000/cached/public ... 61 | # 200 (OK) (GET http://localhost:5000/cached/public) 62 | # Hello World -> DateTime: 8/30/2023 5:06:30 PM 63 | 64 | # Sending request GET http://localhost:5000/cached/public ... 65 | # 200 (OK) (GET http://localhost:5000/cached/public) 66 | # Hello World -> DateTime: 8/30/2023 5:06:30 PM 67 | 68 | # Sending request GET http://localhost:5000/cached/public ... 69 | # 200 (OK) (GET http://localhost:5000/cached/public) 70 | # Hello World -> DateTime: 8/30/2023 5:06:30 PM 71 | 72 | # Sending request GET http://localhost:5000/cached/public ... 73 | # 200 (OK) (GET http://localhost:5000/cached/public) 74 | # Hello World -> DateTime: 8/30/2023 5:06:30 PM 75 | 76 | # The time it took to finish: 77 | # 10.29 seconds 78 | 79 | # ----------------------------------- 80 | # Testing the /cached/private endpoint 81 | 82 | # Sending request GET http://localhost:5000/cached/private ... 83 | # 200 (OK) (GET http://localhost:5000/cached/private) 84 | # Hello World -> DateTime: 8/30/2023 5:06:40 PM 85 | 86 | # Sending request GET http://localhost:5000/cached/private ... 87 | # 200 (OK) (GET http://localhost:5000/cached/private) 88 | # Hello World -> DateTime: 8/30/2023 5:06:46 PM 89 | 90 | # Sending request GET http://localhost:5000/cached/private ... 91 | # 200 (OK) (GET http://localhost:5000/cached/private) 92 | # Hello World -> DateTime: 8/30/2023 5:06:53 PM 93 | 94 | # Sending request GET http://localhost:5000/cached/private ... 95 | # 200 (OK) (GET http://localhost:5000/cached/private) 96 | # Hello World -> DateTime: 8/30/2023 5:06:59 PM 97 | 98 | # Sending request GET http://localhost:5000/cached/private ... 99 | # 200 (OK) (GET http://localhost:5000/cached/private) 100 | # Hello World -> DateTime: 8/30/2023 5:07:05 PM 101 | 102 | # The time it took to finish: 103 | # 30.37 seconds 104 | 105 | # ----------------------------------- 106 | # Testing the /cached/vary/not endpoint 107 | 108 | # Sending request GET http://localhost:5000/cached/vary/not?query1=a&query2=b ... 109 | # 200 (OK) (GET http://localhost:5000/cached/vary/not?query1=a&query2=b) 110 | # Parameters: query1 a query2 b -> DateTime: 8/30/2023 5:07:11 PM 111 | 112 | # Sending request GET http://localhost:5000/cached/vary/not?query1=a&query2=b ... 113 | # 200 (OK) (GET http://localhost:5000/cached/vary/not?query1=a&query2=b) 114 | # Parameters: query1 a query2 b -> DateTime: 8/30/2023 5:07:11 PM 115 | 116 | # Sending request GET http://localhost:5000/cached/vary/not?query1=c&query2=d ... 117 | # 200 (OK) (GET http://localhost:5000/cached/vary/not?query1=c&query2=d) 118 | # Parameters: query1 a query2 b -> DateTime: 8/30/2023 5:07:11 PM 119 | 120 | # Sending request GET http://localhost:5000/cached/vary/not?query1=c&query2=d ... 121 | # 200 (OK) (GET http://localhost:5000/cached/vary/not?query1=c&query2=d) 122 | # Parameters: query1 a query2 b -> DateTime: 8/30/2023 5:07:11 PM 123 | 124 | # The time it took to finish: 125 | # 9.22 seconds 126 | 127 | # ----------------------------------- 128 | # Testing the /cached/vary/yes endpoint 129 | 130 | # Sending request GET http://localhost:5000/cached/vary/yes?query1=a&query2=b ... 131 | # 200 (OK) (GET http://localhost:5000/cached/vary/yes?query1=a&query2=b) 132 | # Parameters: query1 a query2 b -> DateTime: 8/30/2023 5:07:20 PM 133 | 134 | # Sending request GET http://localhost:5000/cached/vary/yes?query1=a&query2=b ... 135 | # 200 (OK) (GET http://localhost:5000/cached/vary/yes?query1=a&query2=b) 136 | # Parameters: query1 a query2 b -> DateTime: 8/30/2023 5:07:20 PM 137 | 138 | # Sending request GET http://localhost:5000/cached/vary/yes?query1=c&query2=d ... 139 | # 200 (OK) (GET http://localhost:5000/cached/vary/yes?query1=c&query2=d) 140 | # Parameters: query1 c query2 d -> DateTime: 8/30/2023 5:07:27 PM 141 | 142 | # Sending request GET http://localhost:5000/cached/vary/yes?query1=c&query2=d ... 143 | # 200 (OK) (GET http://localhost:5000/cached/vary/yes?query1=c&query2=d) 144 | # Parameters: query1 c query2 d -> DateTime: 8/30/2023 5:07:27 PM 145 | 146 | # The time it took to finish: 147 | # 14.22 seconds 148 | ``` 149 | 150 | Notice that at this example, the cache worked properly only for the `/cached/public` and `/cached/vary/yes` endpoints, as expected. You can read the documentation presented before to understand why. 151 | 152 | One last information, notice that the server will inform whenever the response was cached or not, just check the logs. 153 | 154 | For example: 155 | 156 | * If the response was cached: `The response has been cached.` and `Serving response from cache.`; 157 | * If the response was not cached: `The response could not be cached for this request.` and `No cached response available for this request.`. -------------------------------------------------------------------------------- /src/Giraffe/Csrf.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe 2 | 3 | /// 4 | /// CSRF (Cross-Site Request Forgery) protection helpers for Giraffe. 5 | /// Provides anti-forgery token generation and validation. 6 | /// 7 | [] 8 | module Csrf = 9 | open System 10 | open System.Security.Cryptography 11 | open System.Text 12 | open System.Threading.Tasks 13 | open Microsoft.AspNetCore.Http 14 | open Microsoft.Extensions.Logging 15 | open Microsoft.AspNetCore.Antiforgery 16 | 17 | // Defaults are selected to what developers would expect from ASP.NET Core application. 18 | 19 | /// 20 | /// Default CSRF token header name 21 | /// 22 | [] 23 | let DefaultCsrfTokenHeaderName = "X-CSRF-TOKEN" 24 | 25 | /// 26 | /// Default CSRF token form field name 27 | /// 28 | [] 29 | let DefaultCsrfTokenFormFieldName = "__RequestVerificationToken" 30 | 31 | /// 32 | /// Validates the CSRF token from the request. 33 | /// Checks for token in header (X-CSRF-TOKEN) or form field (__RequestVerificationToken). 34 | /// 35 | /// Optional custom handler for invalid tokens. If None, returns 403 Forbidden with logged warning. 36 | /// The next HttpFunc 37 | /// The HttpContext 38 | /// HttpFuncResult 39 | let validateCsrfTokenExt (invalidTokenHandler: HttpHandler option) : HttpHandler = 40 | fun (next: HttpFunc) (ctx: HttpContext) -> 41 | task { 42 | let antiforgery = ctx.GetService() 43 | 44 | try 45 | let! isValid = antiforgery.IsRequestValidAsync ctx 46 | 47 | if isValid then 48 | return! next ctx 49 | else 50 | let defaultHandler = 51 | fun (next: HttpFunc) (ctx: HttpContext) -> 52 | let logger = ctx.GetLogger("Giraffe.Csrf") 53 | 54 | logger.LogWarning( 55 | "CSRF token validation failed for request to {Path}", 56 | ctx.Request.Path 57 | ) 58 | 59 | ctx.Response.StatusCode <- 403 60 | Task.FromResult(Some ctx) 61 | 62 | let handler = invalidTokenHandler |> Option.defaultValue defaultHandler 63 | return! handler earlyReturn ctx 64 | with ex -> 65 | let defaultHandler = 66 | fun (next: HttpFunc) (ctx: HttpContext) -> 67 | let logger = ctx.GetLogger("Giraffe.Csrf") 68 | logger.LogWarning(ex, "CSRF token validation error for request to {Path}", ctx.Request.Path) 69 | ctx.Response.StatusCode <- 403 70 | Task.FromResult(Some ctx) 71 | 72 | let handler = invalidTokenHandler |> Option.defaultValue defaultHandler 73 | return! handler earlyReturn ctx 74 | } 75 | 76 | /// 77 | /// Validates the CSRF token from the request with default error handling. 78 | /// Checks for token in header (X-CSRF-TOKEN) or form field (__RequestVerificationToken). 79 | /// Uses default error handling (403 Forbidden) for invalid tokens. 80 | /// 81 | /// The next HttpFunc 82 | /// The HttpContext 83 | /// HttpFuncResult 84 | let validateCsrfToken: HttpHandler = validateCsrfTokenExt None 85 | 86 | /// 87 | /// Alias for validateCsrfToken - validates anti-forgery tokens from requests. 88 | /// 89 | let requireAntiforgeryToken = validateCsrfToken 90 | 91 | /// 92 | /// Alias for validateCsrfTokenExt - validates anti-forgery tokens from requests with custom error handler. 93 | /// 94 | let requireAntiforgeryTokenExt = validateCsrfTokenExt 95 | 96 | /// 97 | /// Generates a CSRF token and adds it to the HttpContext items for use in views. 98 | /// The token can be accessed via ctx.Items["CsrfToken"] and ctx.Items["CsrfTokenHeaderName"]. 99 | /// 100 | /// The next HttpFunc 101 | /// The HttpContext 102 | /// HttpFuncResult 103 | let generateCsrfToken: HttpHandler = 104 | fun (next: HttpFunc) (ctx: HttpContext) -> 105 | task { 106 | let antiforgery = ctx.GetService() 107 | let tokens = antiforgery.GetAndStoreTokens ctx 108 | 109 | // Store token for view rendering 110 | ctx.Items.["CsrfToken"] <- tokens.RequestToken 111 | ctx.Items.["CsrfTokenHeaderName"] <- tokens.HeaderName 112 | 113 | return! next ctx 114 | } 115 | 116 | /// 117 | /// Returns the CSRF token as JSON for AJAX requests. 118 | /// Response format: { "token": "...", "headerName": "X-CSRF-TOKEN" } 119 | /// 120 | /// The next HttpFunc 121 | /// The HttpContext 122 | /// HttpFuncResult 123 | let csrfTokenJson: HttpHandler = 124 | fun (next: HttpFunc) (ctx: HttpContext) -> 125 | task { 126 | let antiforgery = ctx.GetService() 127 | let tokens = antiforgery.GetAndStoreTokens ctx 128 | 129 | let response = 130 | {| 131 | token = tokens.RequestToken 132 | headerName = tokens.HeaderName 133 | |} 134 | 135 | return! Core.json response next ctx 136 | } 137 | 138 | /// 139 | /// Returns the CSRF token as an HTML hidden input field. 140 | /// Can be included directly in forms. 141 | /// 142 | /// The next HttpFunc 143 | /// The HttpContext 144 | /// HttpFuncResult 145 | let csrfTokenHtml: HttpHandler = 146 | fun (next: HttpFunc) (ctx: HttpContext) -> 147 | task { 148 | let antiforgery = ctx.GetService() 149 | let tokens = antiforgery.GetAndStoreTokens(ctx) 150 | 151 | let html = 152 | sprintf "" tokens.HeaderName tokens.RequestToken 153 | 154 | return! Core.htmlString html next ctx 155 | } 156 | -------------------------------------------------------------------------------- /src/Giraffe/RequestLimitation.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Giraffe.RequestLimitation 3 | 4 | open System 5 | open Microsoft.AspNetCore.Http 6 | 7 | /// 8 | /// Use this record to specify your custom error handlers. If you use the Option.None value, we'll use the default 9 | /// handlers that changes the status code to 406 (not acceptable) and responds with a piece of text. 10 | /// 11 | type OptionalErrorHandlers = 12 | { 13 | InvalidHeaderValue: HttpHandler option 14 | HeaderNotFound: HttpHandler option 15 | } 16 | 17 | /// 18 | /// Filters an incoming HTTP request based on the accepted mime types of the client (Accept HTTP header). 19 | /// If the client doesn't accept any of the provided mimeTypes then the handler will not continue executing the next function. 20 | /// 21 | /// List of mime types of which the client has to accept at least one. 22 | /// OptionalErrorHandlers record with HttpHandler options to define the server 23 | /// response either if the header does not exist or has an invalid value. If both are `Option.None`, we use default 24 | /// handlers. 25 | /// 26 | /// 27 | /// A Giraffe function which can be composed into a bigger web application. 28 | let mustAcceptAny (mimeTypes: string list) (optionalErrorHandler: OptionalErrorHandlers) : HttpHandler = 29 | fun (next: HttpFunc) (ctx: HttpContext) -> 30 | let headers = ctx.Request.GetTypedHeaders() 31 | 32 | let headerNotFoundHandler = 33 | optionalErrorHandler.HeaderNotFound 34 | |> Option.defaultValue ( 35 | RequestErrors.notAcceptable (text "Request rejected because 'Accept' header was not found") 36 | ) 37 | 38 | let invalidHeaderValueHandler = 39 | optionalErrorHandler.InvalidHeaderValue 40 | |> Option.defaultValue ( 41 | RequestErrors.notAcceptable ( 42 | text "Request rejected because 'Accept' header hasn't got expected MIME type" 43 | ) 44 | ) 45 | 46 | let mimeTypesSet = Set.ofList mimeTypes 47 | 48 | match Option.ofObj (headers.Accept :> _ seq) with 49 | | Some xs when 50 | Seq.map (_.ToString()) xs 51 | |> Set.ofSeq 52 | |> Set.intersect mimeTypesSet 53 | |> (Set.isEmpty >> not) 54 | -> 55 | next ctx 56 | | Some xs when Seq.isEmpty xs -> headerNotFoundHandler earlyReturn ctx 57 | | Some _ -> invalidHeaderValueHandler earlyReturn ctx 58 | | None -> headerNotFoundHandler earlyReturn ctx 59 | 60 | /// 61 | /// Limits to only requests with one of the specified `Content-Type` headers, 62 | /// returning `406 NotAcceptable` when the request header doesn't exists in the set of specified types. 63 | /// 64 | /// The sequence of accepted content types. 65 | /// OptionalErrorHandlers record with HttpHandler options to define the server 66 | /// response either if the header does not exist or has an invalid value. If both are `Option.None`, we use default 67 | /// handlers. 68 | /// A Giraffe function which can be composed into a bigger web application. 69 | let hasAnyContentTypes (contentTypes: string list) (optionalErrorHandler: OptionalErrorHandlers) = 70 | fun (next: HttpFunc) (ctx: HttpContext) -> 71 | let headerNotFoundHandler = 72 | optionalErrorHandler.HeaderNotFound 73 | |> Option.defaultValue ( 74 | RequestErrors.notAcceptable (text "Request rejected because 'Content-Type' header was not found") 75 | ) 76 | 77 | let invalidHeaderValueHandler = 78 | optionalErrorHandler.InvalidHeaderValue 79 | |> Option.defaultValue ( 80 | RequestErrors.notAcceptable ( 81 | text "Request rejected because 'Content-Type' header hasn't got expected value" 82 | ) 83 | ) 84 | 85 | match Option.ofObj ctx.Request.ContentType with 86 | | Some header when List.contains header contentTypes -> next ctx 87 | | Some header when String.IsNullOrEmpty header -> headerNotFoundHandler earlyReturn ctx 88 | | Some _ -> invalidHeaderValueHandler earlyReturn ctx 89 | | None -> headerNotFoundHandler earlyReturn ctx 90 | 91 | 92 | /// 93 | /// Limits to only requests with a specific `Content-Type` header, 94 | /// returning `406 NotAcceptable` when the request header value doesn't match the specified type. 95 | /// 96 | /// The single accepted content type. 97 | /// OptionalErrorHandlers record with HttpHandler options to define the server 98 | /// response either if the header does not exist or has an invalid value. If both are `Option.None`, we use default 99 | /// handlers. 100 | /// A Giraffe function which can be composed into a bigger web application. 101 | let hasContentType (contentType: string) (optionalErrorHandler: OptionalErrorHandlers) = 102 | hasAnyContentTypes [ contentType ] (optionalErrorHandler: OptionalErrorHandlers) 103 | 104 | /// 105 | /// Limits request `Content-Length` header to a specified length, 106 | /// returning `406 NotAcceptable` when no such header is present or the value exceeds the maximum specified length. 107 | /// 108 | /// The maximum accepted length of the incoming request. 109 | /// OptionalErrorHandlers record with HttpHandler options to define the server 110 | /// response either if the header does not exist or has an invalid value. If both are `Option.None`, we use default 111 | /// handlers. 112 | /// A Giraffe function which can be composed into a bigger web application. 113 | let maxContentLength (maxLength: int64) (optionalErrorHandler: OptionalErrorHandlers) = 114 | fun (next: HttpFunc) (ctx: HttpContext) -> 115 | let header = ctx.Request.ContentLength 116 | 117 | let headerNotFoundHandler = 118 | optionalErrorHandler.HeaderNotFound 119 | |> Option.defaultValue ( 120 | RequestErrors.notAcceptable (text "Request rejected because there is no 'Content-Length' header") 121 | ) 122 | 123 | let invalidHeaderValueHandler = 124 | optionalErrorHandler.InvalidHeaderValue 125 | |> Option.defaultValue ( 126 | RequestErrors.notAcceptable (text "Request rejected because 'Content-Length' header is too large") 127 | ) 128 | 129 | match Option.ofNullable header with 130 | | Some v when v <= maxLength -> next ctx 131 | | Some _ -> invalidHeaderValueHandler earlyReturn ctx 132 | | None -> headerNotFoundHandler earlyReturn ctx 133 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | 332 | # macOS 333 | .DS_Store 334 | 335 | # Ionide 336 | .ionide/ 337 | 338 | # Visual Studio Code 339 | .vscode -------------------------------------------------------------------------------- /tests/Giraffe.Tests/Helpers.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Giraffe.Tests.Helpers 3 | 4 | open System 5 | open System.IO 6 | open System.Net 7 | open System.Net.Http 8 | open System.Linq 9 | open System.Text 10 | open System.Threading.Tasks 11 | open System.Xml.Linq 12 | open Microsoft.AspNetCore.Http 13 | open Microsoft.AspNetCore.Hosting 14 | open Microsoft.AspNetCore.TestHost 15 | open Microsoft.AspNetCore.Builder 16 | open Microsoft.Extensions.DependencyInjection 17 | open Microsoft.Extensions.Hosting 18 | open Xunit 19 | open NSubstitute 20 | open Giraffe 21 | 22 | // --------------------------------- 23 | // Common functions 24 | // --------------------------------- 25 | 26 | let toTheoryData xs = 27 | let data = new TheoryData<_>() 28 | 29 | for x in xs do 30 | data.Add x 31 | 32 | data 33 | 34 | let toTheoryData2 xs = 35 | let data = new TheoryData<_, _>() 36 | 37 | for (a, b) in xs do 38 | data.Add(a, b) 39 | 40 | data 41 | 42 | let waitForDebuggerToAttach () = 43 | printfn "Waiting for debugger to attach." 44 | printfn "Press enter when debugger is attached in order to continue test execution..." 45 | Console.ReadLine() |> ignore 46 | 47 | let removeNewLines (html: string) : string = 48 | html.Replace(Environment.NewLine, String.Empty) 49 | 50 | let createETag (eTag: string) = 51 | Some(Microsoft.Net.Http.Headers.EntityTagHeaderValue.FromString false eTag) 52 | 53 | let createWeakETag (eTag: string) = 54 | Some(Microsoft.Net.Http.Headers.EntityTagHeaderValue.FromString true eTag) 55 | 56 | // --------------------------------- 57 | // Assert functions 58 | // --------------------------------- 59 | 60 | let assertFail msg = Assert.True(false, msg) 61 | 62 | let assertFailf format args = 63 | let msg = sprintf format args 64 | Assert.True(false, msg) 65 | 66 | module XmlAssert = 67 | let rec normalize (element: XElement) = 68 | if element.HasElements then 69 | XElement( 70 | element.Name, 71 | element 72 | .Attributes() 73 | .Where(fun a -> a.Name.Namespace = XNamespace.Xmlns) 74 | .OrderBy(fun a -> a.Name.ToString()), 75 | element.Elements().OrderBy(fun a -> a.Name.ToString()).Select(fun e -> normalize (e)) 76 | ) 77 | elif element.IsEmpty then 78 | XElement(element.Name, element.Attributes().OrderBy(fun a -> a.Name.ToString())) 79 | else 80 | XElement(element.Name, element.Attributes().OrderBy(fun a -> a.Name.ToString()), element.Value) 81 | 82 | let equals expectedXml actualXml = 83 | let expectedXElement = XElement.Parse expectedXml |> normalize 84 | let actualXElement = XElement.Parse actualXml |> normalize 85 | Assert.Equal(expectedXElement.ToString(), actualXElement.ToString()) 86 | 87 | // --------------------------------- 88 | // Test server/client setup 89 | // --------------------------------- 90 | 91 | let next: HttpFunc = Some >> Task.FromResult 92 | 93 | let createHost 94 | (configureApp: 'Tuple -> IApplicationBuilder -> unit) 95 | (configureServices: IServiceCollection -> unit) 96 | (args: 'Tuple) 97 | = 98 | #if NET10_0_OR_GREATER 99 | HostBuilder() 100 | .ConfigureWebHost(fun webHostBuilder -> 101 | webHostBuilder 102 | .UseTestServer() 103 | .UseContentRoot(Path.GetFullPath("TestFiles")) 104 | .Configure(Action(configureApp args)) 105 | .ConfigureServices(Action configureServices) 106 | |> ignore 107 | ) 108 | #else 109 | (WebHostBuilder()) 110 | .UseContentRoot(Path.GetFullPath("TestFiles")) 111 | .Configure(Action(configureApp args)) 112 | .ConfigureServices(Action configureServices) 113 | #endif 114 | 115 | let mockJson (ctx: HttpContext) = 116 | 117 | ctx.RequestServices.GetService(typeof).Returns(Json.Serializer(Json.Serializer.DefaultOptions)) 118 | |> ignore 119 | 120 | type NegotiationConfigWithExpectedResult = 121 | { 122 | NegotiationConfig: INegotiationConfig 123 | StatusCode: int 124 | ReturnContentType: string 125 | } 126 | 127 | let mockXml (ctx: HttpContext) = 128 | ctx.RequestServices 129 | .GetService(typeof) 130 | .Returns(SystemXml.Serializer(SystemXml.Serializer.DefaultSettings)) 131 | |> ignore 132 | 133 | let mockNegotiation (ctx: HttpContext) (negotiationConfig: INegotiationConfig) = 134 | ctx.RequestServices.GetService(typeof).Returns(negotiationConfig) 135 | |> ignore 136 | 137 | // --------------------------------- 138 | // Compose web request functions 139 | // --------------------------------- 140 | 141 | let createRequest (method: HttpMethod) (path: string) = 142 | let url = "http://127.0.0.1" + path 143 | new HttpRequestMessage(method, url) 144 | 145 | let makeRequest configureApp configureServices args (request: HttpRequestMessage) = 146 | task { 147 | #if NET10_0_OR_GREATER 148 | // https://github.com/aspnet/Announcements/issues/526 149 | use host = createHost configureApp configureServices args |> _.Build() 150 | 151 | let! _ = host.StartAsync() 152 | use server = host.GetTestServer() 153 | #else 154 | use server = new TestServer(createHost configureApp configureServices args) 155 | #endif 156 | use client = server.CreateClient() 157 | let! response = request |> client.SendAsync 158 | return response 159 | } 160 | 161 | let addHeader (key: string) (value: string) (request: HttpRequestMessage) = 162 | request.Headers.Add(key, value) 163 | request 164 | 165 | // --------------------------------- 166 | // Validate response functions 167 | // --------------------------------- 168 | 169 | let getContentType (response: HttpResponse) = response.Headers.["Content-Type"].[0] 170 | 171 | let getStatusCode (ctx: HttpContext) = ctx.Response.StatusCode 172 | 173 | let isStatus (code: HttpStatusCode) (response: HttpResponseMessage) = 174 | Assert.Equal(code, response.StatusCode) 175 | response 176 | 177 | let containsHeader (flag: bool) (name: string) (response: HttpResponseMessage) = 178 | match flag with 179 | | true -> Assert.True(response.Headers.Contains name) 180 | | false -> Assert.False(response.Headers.Contains name) 181 | 182 | response 183 | 184 | let containsContentHeader (flag: bool) (name: string) (response: HttpResponseMessage) = 185 | match flag with 186 | | true -> Assert.True(response.Content.Headers.Contains name) 187 | | false -> Assert.False(response.Content.Headers.Contains name) 188 | 189 | response 190 | 191 | let hasContentLength (length: int64) (response: HttpResponseMessage) = 192 | Assert.True(response.Content.Headers.ContentLength.HasValue) 193 | Assert.Equal(length, response.Content.Headers.ContentLength.Value) 194 | response 195 | 196 | let hasAcceptRanges (value: string) (response: HttpResponseMessage) = 197 | Assert.Equal(value, response.Headers.AcceptRanges.ToString()) 198 | response 199 | 200 | let hasContentRange (value: string) (response: HttpResponseMessage) = 201 | Assert.Equal(value, response.Content.Headers.ContentRange.ToString()) 202 | response 203 | 204 | let hasETag (eTag: string) (response: HttpResponseMessage) = 205 | Assert.Equal(eTag, (response.Headers.ETag.ToString())) 206 | response 207 | 208 | let hasLastModified (lastModified: DateTimeOffset) (response: HttpResponseMessage) = 209 | Assert.True(response.Content.Headers.LastModified.HasValue) 210 | Assert.Equal(lastModified, response.Content.Headers.LastModified.Value) 211 | response 212 | 213 | let getReqBody (ctx: HttpContext) = 214 | ctx.Request.Body.Position <- 0L 215 | use reader = new StreamReader(ctx.Request.Body, Encoding.UTF8, leaveOpen = true) 216 | reader.ReadToEnd() 217 | 218 | let getBody (ctx: HttpContext) = 219 | ctx.Response.Body.Position <- 0L 220 | use reader = new StreamReader(ctx.Response.Body, Encoding.UTF8, leaveOpen = true) 221 | reader.ReadToEnd() 222 | 223 | let readText (response: HttpResponseMessage) = response.Content.ReadAsStringAsync() 224 | 225 | let readBytes (response: HttpResponseMessage) = response.Content.ReadAsByteArrayAsync() 226 | 227 | let printBytes (bytes: byte[]) = 228 | bytes 229 | |> Array.fold 230 | (fun (s: string) (b: byte) -> 231 | match s.Length with 232 | | 0 -> sprintf "%i" b 233 | | _ -> sprintf "%s,%i" s b 234 | ) 235 | "" 236 | 237 | let shouldBeEmpty (bytes: byte[]) = Assert.True(bytes.Length.Equals 0) 238 | 239 | let shouldEqual (expected: string) actual = Assert.Equal(expected, actual) 240 | -------------------------------------------------------------------------------- /src/Giraffe/Auth.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe 2 | 3 | [] 4 | module Auth = 5 | open System 6 | open System.Security.Claims 7 | open Microsoft.AspNetCore.Http 8 | open Microsoft.AspNetCore.Authentication 9 | open Microsoft.AspNetCore.Authorization 10 | 11 | /// 12 | /// Challenges a client to authenticate via a specific authScheme. 13 | /// 14 | /// The name of an authentication scheme from your application. 15 | /// 16 | /// 17 | /// A Giraffe function which can be composed into a bigger web application. 18 | let challenge (authScheme: string) : HttpHandler = 19 | fun (next: HttpFunc) (ctx: HttpContext) -> 20 | task { 21 | do! ctx.ChallengeAsync authScheme 22 | return! next ctx 23 | } 24 | 25 | /// 26 | /// Signs out the currently logged in user via the provided authScheme. 27 | /// 28 | /// The name of an authentication scheme from your application. 29 | /// 30 | /// 31 | /// A Giraffe function which can be composed into a bigger web application. 32 | let signOut (authScheme: string) : HttpHandler = 33 | fun (next: HttpFunc) (ctx: HttpContext) -> 34 | task { 35 | do! ctx.SignOutAsync authScheme 36 | return! next ctx 37 | } 38 | 39 | /// 40 | /// Validates if a satisfies a certain condition. If the policy returns true then it will continue with the next function otherwise it will short circuit and execute the authFailedHandler. 41 | /// 42 | /// One or many conditions which a must meet. The policy function should return true on success and false on failure. 43 | /// A function which will be executed when the policy returns false. 44 | /// 45 | /// 46 | /// A Giraffe function which can be composed into a bigger web application. 47 | let authorizeRequest (predicate: HttpContext -> bool) (authFailedHandler: HttpHandler) : HttpHandler = 48 | fun (next: HttpFunc) (ctx: HttpContext) -> 49 | (if predicate ctx then 50 | next 51 | else 52 | authFailedHandler earlyReturn) 53 | ctx 54 | 55 | [] 56 | /// 57 | /// Validates if a satisfies a certain condition. If the policy returns true then it will continue with the next function otherwise it will short circuit and execute the authFailedHandler. 58 | /// 59 | /// One or many conditions which a must meet. The policy function should return true on success and false on failure. 60 | /// A function which will be executed when the policy returns false. 61 | /// A Giraffe function which can be composed into a bigger web application. 62 | let evaluateUserPolicy (policy: ClaimsPrincipal -> bool) (authFailedHandler: HttpHandler) : HttpHandler = 63 | authorizeRequest (fun ctx -> policy ctx.User) authFailedHandler 64 | 65 | /// 66 | /// Validates if a satisfies a certain condition. If the policy returns true then it will continue with the next function otherwise it will short circuit and execute the authFailedHandler. 67 | /// 68 | /// A Giraffe function which can be composed into a bigger web application. 69 | let authorizeUser = evaluateUserPolicy 70 | 71 | /// 72 | /// Validates if a user has successfully authenticated. This function checks if the auth middleware was able to establish a user's identity by validating certain parts of the HTTP request (e.g. a cookie or a token) and set the object of the . 73 | /// 74 | /// A function which will be executed when authentication failed. 75 | /// A Giraffe function which can be composed into a bigger web application. 76 | let requiresAuthentication (authFailedHandler: HttpHandler) : HttpHandler = 77 | authorizeUser 78 | (fun user -> isNotNull user && isNotNull user.Identity && user.Identity.IsAuthenticated) 79 | authFailedHandler 80 | 81 | /// 82 | /// Validates if a user is a member of a specific role. 83 | /// 84 | /// The required role of which a user must be a member of in order to pass the validation. 85 | /// A function which will be executed when validation fails. 86 | /// A Giraffe function which can be composed into a bigger web application. 87 | let requiresRole (role: string) (authFailedHandler: HttpHandler) : HttpHandler = 88 | authorizeUser (fun user -> user.IsInRole role) authFailedHandler 89 | 90 | /// 91 | /// Validates if a user is a member of at least one of a given list of roles. 92 | /// 93 | /// A list of roles of which a user must be a member of (minimum one) in order to pass the validation. 94 | /// A function which will be executed when validation fails. 95 | /// A Giraffe function which can be composed into a bigger web application. 96 | let requiresRoleOf (roles: string list) (authFailedHandler: HttpHandler) : HttpHandler = 97 | authorizeUser (fun user -> List.exists user.IsInRole roles) authFailedHandler 98 | 99 | /// 100 | /// Validates if a user meets a given authorization policy. 101 | /// 102 | /// The name of an which a user must meet in order to pass the validation. 103 | /// A function which will be executed when validation fails. 104 | /// 105 | /// 106 | /// A Giraffe function which can be composed into a bigger web application. 107 | let authorizeByPolicyName (policyName: string) (authFailedHandler: HttpHandler) : HttpHandler = 108 | fun (next: HttpFunc) (ctx: HttpContext) -> 109 | task { 110 | let authService = ctx.GetService() 111 | let! result = authService.AuthorizeAsync(ctx.User, policyName) 112 | 113 | return! 114 | (if result.Succeeded then 115 | next 116 | else 117 | authFailedHandler earlyReturn) 118 | ctx 119 | } 120 | 121 | /// 122 | /// Validates if a user meets a given authorization policy. 123 | /// 124 | /// The name of an which a user must meet in order to pass the validation. 125 | /// A function which will be executed when validation fails. 126 | /// 127 | /// 128 | /// A Giraffe function which can be composed into a bigger web application. 129 | let authorizeByPolicy (policy: AuthorizationPolicy) (authFailedHandler: HttpHandler) : HttpHandler = 130 | fun (next: HttpFunc) (ctx: HttpContext) -> 131 | task { 132 | let authService = ctx.GetService() 133 | let! result = authService.AuthorizeAsync(ctx.User, policy) 134 | 135 | return! 136 | (if result.Succeeded then 137 | next 138 | else 139 | authFailedHandler earlyReturn) 140 | ctx 141 | } 142 | -------------------------------------------------------------------------------- /src/Giraffe/Negotiation.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Giraffe.Negotiation 3 | 4 | open System 5 | open System.Collections.Generic 6 | open System.Runtime.CompilerServices 7 | open Microsoft.AspNetCore.Http 8 | 9 | // --------------------------- 10 | // Configuration types 11 | // --------------------------- 12 | 13 | /// 14 | /// Interface defining the negotiation rules and the for unacceptable requests when doing content negotiation in Giraffe. 15 | /// 16 | type INegotiationConfig = 17 | /// 18 | /// A dictionary of mime types and response writing functions. 19 | /// 20 | /// Each mime type must be mapped to a function which accepts an obj and returns a which will send a response in the associated mime type. 21 | /// 22 | /// 23 | /// 24 | /// dict [ "application/json", json; "application/xml" , xml ] 25 | /// 26 | /// 27 | abstract member Rules: IDictionary HttpHandler> 28 | 29 | /// 30 | /// A function which will be invoked if none of the accepted mime types can be satisfied. Generally this would send a response with a status code of 406 Unacceptable. 31 | /// 32 | /// 33 | abstract member UnacceptableHandler: HttpHandler 34 | 35 | let private unacceptableHandler = 36 | fun (next: HttpFunc) (ctx: HttpContext) -> 37 | (setStatusCode 406 38 | >=> ((ctx.Request.Headers.["Accept"]).ToString() 39 | |> sprintf "%s is unacceptable by the server." 40 | |> text)) 41 | next 42 | ctx 43 | 44 | /// 45 | /// The default implementation of 46 | /// 47 | /// Supported mime types: 48 | /// 49 | /// */*: If a client accepts any content type then the server will return a JSON response. 50 | /// 51 | /// application/json: Server will send a JSON response. 52 | /// 53 | /// application/xml: Server will send an XML response. 54 | /// 55 | /// text/xml: Server will send an XML response. 56 | /// 57 | /// text/plain: Server will send a plain text response (by suing an object's ToString() method). 58 | /// 59 | type DefaultNegotiationConfig() = 60 | interface INegotiationConfig with 61 | member __.Rules = 62 | dict 63 | [ 64 | "*/*", json 65 | "application/json", json 66 | "application/xml", xml 67 | "text/xml", xml 68 | "text/plain", (fun x -> x.ToString() |> text) 69 | ] 70 | 71 | member __.UnacceptableHandler = unacceptableHandler 72 | 73 | 74 | /// 75 | /// An implementation of INegotiationConfig which allows returning JSON only. 76 | /// 77 | /// Supported mime types: 78 | /// 79 | /// */*: If a client accepts any content type then the server will return a JSON response. 80 | /// application/json: Server will send a JSON response. 81 | /// 82 | type JsonOnlyNegotiationConfig() = 83 | interface INegotiationConfig with 84 | member __.Rules = dict [ "*/*", json; "application/json", json ] 85 | member __.UnacceptableHandler = unacceptableHandler 86 | 87 | // --------------------------- 88 | // HttpContext extensions 89 | // --------------------------- 90 | 91 | [] 92 | type NegotiationExtensions() = 93 | /// 94 | /// Sends a response back to the client based on the request's Accept header. 95 | /// 96 | /// If the Accept header cannot be matched with one of the supported mime types from the negotiationRules then the unacceptableHandler will be invoked. 97 | /// 98 | /// The current http context object. 99 | /// A dictionary of mime types and response writing functions. Each mime type must be mapped to a function which accepts an obj and returns a which will send a response in the associated mime type (e.g.: dict [ "application/json", json; "application/xml" , xml ]). 100 | /// A function which will be invoked if none of the accepted mime types can be satisfied. Generally this would send a response with a status code of 406 Unacceptable. 101 | /// The object to send back to the client. 102 | /// Task of Some HttpContext after writing to the body of the response. 103 | [] 104 | static member NegotiateWithAsync 105 | ( 106 | ctx: HttpContext, 107 | negotiationRules: IDictionary HttpHandler>, 108 | unacceptableHandler: HttpHandler, 109 | responseObj: obj 110 | ) = 111 | let acceptedMimeTypes = (ctx.Request.GetTypedHeaders()).Accept 112 | 113 | if isNull acceptedMimeTypes || acceptedMimeTypes.Count = 0 then 114 | let kv = negotiationRules |> Seq.head 115 | kv.Value responseObj earlyReturn ctx 116 | else 117 | let mutable mimeType = Unchecked.defaultof<_> 118 | let mutable bestQuality = Double.NegativeInfinity 119 | let mutable currQuality = 1. 120 | // Filter the list of acceptedMimeTypes by the negotiationRules 121 | // and selects the mimetype with the greatest quality 122 | for x in acceptedMimeTypes do 123 | if negotiationRules.ContainsKey x.MediaType.Value then 124 | currQuality <- (if x.Quality.HasValue then x.Quality.Value else 1.) 125 | 126 | if bestQuality < currQuality then 127 | bestQuality <- currQuality 128 | mimeType <- x 129 | 130 | if isNull mimeType then 131 | unacceptableHandler earlyReturn ctx 132 | else 133 | negotiationRules.[mimeType.MediaType.Value] responseObj earlyReturn ctx 134 | 135 | /// 136 | /// Sends a response back to the client based on the request's Accept header. 137 | /// 138 | /// The negotiation rules as well as a for unacceptable requests can be configured in the ASP.NET Core startup code by registering a custom class of type . 139 | /// 140 | /// The current http context object. 141 | /// The object to send back to the client. 142 | /// Task of Some HttpContext after writing to the body of the response. 143 | [] 144 | static member NegotiateAsync(ctx: HttpContext, responseObj: obj) = 145 | let config = ctx.GetService() 146 | ctx.NegotiateWithAsync(config.Rules, config.UnacceptableHandler, responseObj) 147 | 148 | // --------------------------- 149 | // HttpHandler functions 150 | // --------------------------- 151 | 152 | /// 153 | /// Sends a response back to the client based on the request's Accept header. 154 | /// 155 | /// If the Accept header cannot be matched with one of the supported mime types from the negotiationRules then the unacceptableHandler will be invoked. 156 | /// 157 | /// A dictionary of mime types and response writing functions. Each mime type must be mapped to a function which accepts an obj and returns a which will send a response in the associated mime type (e.g.: dict [ "application/json", json; "application/xml" , xml ]). 158 | /// A function which will be invoked if none of the accepted mime types can be satisfied. Generally this would send a response with a status code of 406 Unacceptable. 159 | /// The object to send back to the client. 160 | /// 161 | /// A Giraffe function which can be composed into a bigger web application. 162 | let negotiateWith 163 | (negotiationRules: IDictionary HttpHandler>) 164 | (unacceptableHandler: HttpHandler) 165 | (responseObj: obj) 166 | : HttpHandler = 167 | fun (_: HttpFunc) (ctx: HttpContext) -> ctx.NegotiateWithAsync(negotiationRules, unacceptableHandler, responseObj) 168 | 169 | /// 170 | /// Sends a response back to the client based on the request's Accept header. 171 | /// 172 | /// The negotiation rules as well as a for unacceptable requests can be configured in the ASP.NET Core startup code by registering a custom class of type . 173 | /// 174 | /// The object to send back to the client. 175 | /// 176 | /// A Giraffe function which can be composed into a bigger web application. 177 | let negotiate (responseObj: obj) : HttpHandler = 178 | fun (_: HttpFunc) (ctx: HttpContext) -> ctx.NegotiateAsync responseObj 179 | -------------------------------------------------------------------------------- /tests/Giraffe.Tests/FormatExpressionTests.fs: -------------------------------------------------------------------------------- 1 | module Giraffe.Tests.FormatExpressionTests 2 | 3 | open System 4 | open Xunit 5 | open Giraffe.FormatExpressions 6 | 7 | // --------------------------------- 8 | // Positive Tests 9 | // --------------------------------- 10 | 11 | [] 12 | let ``Simple matching format string returns correct tuple`` () = 13 | tryMatchInputExact "/foo/%s/bar/%c/%b/test/%i" false "/foo/john/bar/M/true/test/123" 14 | |> function 15 | | None -> assertFail "Format failed to match input." 16 | | Some(s1: string, c1: char, b1: bool, i1: int) -> 17 | Assert.Equal("john", s1) 18 | Assert.Equal('M', c1) 19 | Assert.True(b1) 20 | Assert.Equal(123, i1) 21 | 22 | [] 23 | let ``Format string with escaped "%" returns correct tuple`` () = 24 | tryMatchInputExact "/foo/%%s/%%%s/%c/%b/test/%i" false "/foo/%s/%bar/M/true/test/123" 25 | |> function 26 | | None -> assertFail "Format failed to match input." 27 | | Some(s1: string, c1: char, b1: bool, i1: int) -> 28 | Assert.Equal("bar", s1) 29 | Assert.Equal('M', c1) 30 | Assert.True(b1) 31 | Assert.Equal(123, i1) 32 | 33 | [] 34 | let ``Format string with regex symbols returns correct tuple`` () = 35 | tryMatchInputExact "/foo/(.+)/%s/bar/%d/(.+)" false "/foo/(.+)/!£$%^&*(/bar/-345/(.+)" 36 | |> function 37 | | None -> assertFail "Format failed to match input." 38 | | Some(s1: string, d1: int64) -> 39 | Assert.Equal("!£$%^&*(", s1) 40 | Assert.Equal(-345L, d1) 41 | 42 | [] 43 | let ``Format string with single "%s" matches a single string`` () = 44 | tryMatchInputExact "%s" false "hello world !!" 45 | |> function 46 | | None -> assertFail "Format failed to match input." 47 | | Some(s: string) -> Assert.Equal("hello world !!", s) 48 | 49 | [] 50 | let ``Format string with single "%b" matches "true"`` () = 51 | tryMatchInputExact "%b" false "true" 52 | |> function 53 | | None -> assertFail "Format failed to match input." 54 | | Some(b1: bool) -> Assert.True(b1) 55 | 56 | [] 57 | let ``Format string with single "%b" matches "false"`` () = 58 | tryMatchInputExact "%b" false "false" 59 | |> function 60 | | None -> assertFail "Format failed to match input." 61 | | Some(b: bool) -> Assert.False(b) 62 | 63 | [] 64 | let ``Format string with single "%b" matches "TRUE"`` () = 65 | tryMatchInputExact "%b" false "TRUE" 66 | |> function 67 | | None -> assertFail "Format failed to match input." 68 | | Some(b: bool) -> Assert.True(b) 69 | 70 | [] 71 | let ``Format string with single "%b" matches "FALSE"`` () = 72 | tryMatchInputExact "%b" false "FALSE" 73 | |> function 74 | | None -> assertFail "Format failed to match input." 75 | | Some(b: bool) -> Assert.False(b) 76 | 77 | [] 78 | let ``Format string with single "%b" matches "True"`` () = 79 | tryMatchInputExact "%b" false "True" 80 | |> function 81 | | None -> assertFail "Format failed to match input." 82 | | Some(b: bool) -> Assert.True(b) 83 | 84 | [] 85 | let ``Format string with single "%b" matches "False"`` () = 86 | tryMatchInputExact "%b" false "False" 87 | |> function 88 | | None -> assertFail "Format failed to match input." 89 | | Some(b: bool) -> Assert.False(b) 90 | 91 | [] 92 | let ``Format string with single "%b" matches "tRuE"`` () = 93 | tryMatchInputExact "%b" false "tRuE" 94 | |> function 95 | | None -> assertFail "Format failed to match input." 96 | | Some(b: bool) -> Assert.True(b) 97 | 98 | [] 99 | let ``Format string with single "%i" matches "0"`` () = 100 | tryMatchInputExact "%i" false "0" 101 | |> function 102 | | None -> assertFail "Format failed to match input." 103 | | Some(i: int) -> Assert.Equal(0, i) 104 | 105 | [] 106 | let ``Format string with single "%i" matches int32 min value`` () = 107 | tryMatchInputExact "%i" false "-2147483648" 108 | |> function 109 | | None -> assertFail "Format failed to match input." 110 | | Some(i: int) -> Assert.Equal(Int32.MinValue, i) 111 | 112 | [] 113 | let ``Format string with single "%i" matches int32 max value`` () = 114 | tryMatchInputExact "%i" false "2147483647" 115 | |> function 116 | | None -> assertFail "Format failed to match input." 117 | | Some(i: int) -> Assert.Equal(Int32.MaxValue, i) 118 | 119 | [] 120 | let ``Format string with single "%d" matches "0"`` () = 121 | tryMatchInputExact "%d" false "0" 122 | |> function 123 | | None -> assertFail "Format failed to match input." 124 | | Some(d: int64) -> Assert.Equal(0L, d) 125 | 126 | [] 127 | let ``Format string with single "%d" matches int64 min value`` () = 128 | tryMatchInputExact "%d" false "-9223372036854775808" 129 | |> function 130 | | None -> assertFail "Format failed to match input." 131 | | Some(d: int64) -> Assert.Equal(Int64.MinValue, d) 132 | 133 | [] 134 | let ``Format string with single "%d" matches int64 max value`` () = 135 | tryMatchInputExact "%d" false "9223372036854775807" 136 | |> function 137 | | None -> assertFail "Format failed to match input." 138 | | Some(d: int64) -> Assert.Equal(Int64.MaxValue, d) 139 | 140 | [] 141 | let ``Format string with single "%f" matches "0.0"`` () = 142 | tryMatchInputExact "%f" false "0.0" 143 | |> function 144 | | None -> assertFail "Format failed to match input." 145 | | Some(f: float) -> Assert.Equal(0.0, f) 146 | 147 | [] 148 | let ``Format string with single "%f" matches "0.5"`` () = 149 | tryMatchInputExact "%f" false "0.5" 150 | |> function 151 | | None -> assertFail "Format failed to match input." 152 | | Some(f: float) -> Assert.Equal(0.5, f) 153 | 154 | [] 155 | let ``Format string with single "%f" matches "100500.7895"`` () = 156 | tryMatchInputExact "%f" false "100500.7895" 157 | |> function 158 | | None -> assertFail "Format failed to match input." 159 | | Some(f: float) -> Assert.Equal(100500.7895, f) 160 | 161 | [] 162 | let ``Format string with single "%f" matches "-45.342"`` () = 163 | tryMatchInputExact "%f" false "-45.342" 164 | |> function 165 | | None -> assertFail "Format failed to match input." 166 | | Some(f: float) -> Assert.Equal(-45.342, f) 167 | 168 | [] 169 | let ``Format string with single "%O" matches "00000000-0000-0000-0000-000000000000"`` () = 170 | tryMatchInputExact "%O" false "00000000-0000-0000-0000-000000000000" 171 | |> function 172 | | None -> assertFail "Format failed to match input." 173 | | Some g -> Assert.Equal(Guid.Empty, g) 174 | 175 | [] 176 | let ``Format string with single "%O" matches "FE9CFE19-35D4-4EDC-9A95-5D38C4D579BD"`` () = 177 | tryMatchInputExact "%O" false "FE9CFE19-35D4-4EDC-9A95-5D38C4D579BD" 178 | |> function 179 | | None -> assertFail "Format failed to match input." 180 | | Some g -> Assert.Equal(Guid("FE9CFE19-35D4-4EDC-9A95-5D38C4D579BD"), g) 181 | 182 | [] 183 | let ``Format string with single "%O" matches "00000000000000000000000000000000"`` () = 184 | tryMatchInputExact "%O" false "00000000000000000000000000000000" 185 | |> function 186 | | None -> assertFail "Format failed to match input." 187 | | Some g -> Assert.Equal(Guid.Empty, g) 188 | 189 | [] 190 | let ``Format string with single "%O" matches "FE9CFE1935D44EDC9A955D38C4D579BD"`` () = 191 | tryMatchInputExact "%O" false "FE9CFE1935D44EDC9A955D38C4D579BD" 192 | |> function 193 | | None -> assertFail "Format failed to match input." 194 | | Some g -> Assert.Equal(Guid("FE9CFE19-35D4-4EDC-9A95-5D38C4D579BD"), g) 195 | 196 | [] 197 | let ``Format string with single "%O" matches "Xy0MVKupFES9NpmZ9TiHcw"`` () = 198 | tryMatchInputExact "%O" false "Xy0MVKupFES9NpmZ9TiHcw" 199 | |> function 200 | | None -> assertFail "Format failed to match input." 201 | | Some g -> Assert.Equal(Guid("540c2d5f-a9ab-4414-bd36-9999f5388773"), g) 202 | 203 | [] 204 | let ``Format string with single "%u" matches "FOwfPLe6waQ"`` () = 205 | tryMatchInputExact "%u" false "FOwfPLe6waQ" 206 | |> function 207 | | None -> assertFail "Format failed to match input." 208 | | Some id -> Assert.Equal(1507614320903242148UL, id) 209 | 210 | [] 211 | let ``Format string with "%s" matches url encoded string`` () = 212 | tryMatchInputExact "/encode/%s" false "/encode/a%2fb%2Bc.d%2Ce" 213 | |> function 214 | | None -> assertFail "Format failed to match input." 215 | | Some(s: string) -> Assert.Equal("a/b%2Bc.d%2Ce", s) 216 | 217 | // --------------------------------- 218 | // Negative Tests 219 | // --------------------------------- 220 | 221 | [] 222 | let ``Format string with single "%s" doesn't matches empty string`` () = 223 | tryMatchInputExact "%s" false "" 224 | |> function 225 | | None -> () 226 | | Some _ -> assertFail "Should not have matched string" 227 | 228 | [] 229 | let ``Format string with single "%i" doesn't match int32 max value + 1`` () = 230 | tryMatchInputExact "%i" false "2147483648" 231 | |> function 232 | | None -> () 233 | | Some _ -> assertFail "Should not have matched string" 234 | 235 | [] 236 | let ``Format string with single "%f" doesn't match "0"`` () = 237 | tryMatchInputExact "%f" false "0" 238 | |> function 239 | | None -> () 240 | | Some _ -> assertFail "Should not have matched string" 241 | -------------------------------------------------------------------------------- /src/Giraffe/Preconditional.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Giraffe.Preconditional 3 | 4 | open System 5 | open System.Linq 6 | open System.Runtime.CompilerServices 7 | open Microsoft.AspNetCore.Http 8 | open Microsoft.AspNetCore.Http.Headers 9 | open Microsoft.Extensions.Primitives 10 | open Microsoft.Net.Http.Headers 11 | 12 | type Precondition = 13 | | NoConditionsSpecified 14 | | ResourceNotModified 15 | | ConditionFailed 16 | | AllConditionsMet 17 | 18 | type EntityTagHeaderValue with 19 | /// 20 | /// Creates an object of type . 21 | /// 22 | /// The difference between a regular (strong) ETag and a weak ETag is that a matching strong ETag guarantees the file is byte-for-byte identical, whereas a matching weak ETag indicates that the content is semantically the same. So if the content of the file changes, the weak ETag should change as well. 23 | /// The entity tag value (without quotes or the W/ prefix). 24 | /// Returns an object of . 25 | static member FromString (isWeak: bool) (eTag: string) = 26 | let eTagValue = sprintf "\"%s\"" eTag 27 | EntityTagHeaderValue(StringSegment(eTagValue), isWeak) 28 | 29 | type HttpContext with 30 | 31 | member private this.IsHeadOrGetRequest() = 32 | HttpMethods.IsHead this.Request.Method || HttpMethods.IsGet this.Request.Method 33 | 34 | member private this.ValidateIfMatch (eTag: EntityTagHeaderValue option) (requestHeaders: RequestHeaders) = 35 | match isNotNull requestHeaders.IfMatch && requestHeaders.IfMatch.Any() with 36 | | false -> NoConditionsSpecified 37 | | true -> 38 | match eTag with 39 | | None -> ConditionFailed 40 | | Some eTag -> 41 | requestHeaders.IfMatch 42 | |> Seq.exists (fun t -> t.Compare(eTag, true)) 43 | |> function 44 | | true -> AllConditionsMet 45 | | false -> ConditionFailed 46 | 47 | member private this.ValidateIfUnmodifiedSince 48 | (lastModified: DateTimeOffset option) 49 | (requestHeaders: RequestHeaders) 50 | = 51 | match requestHeaders.IfUnmodifiedSince.HasValue with 52 | | false -> NoConditionsSpecified 53 | | true -> 54 | match lastModified with 55 | | None -> AllConditionsMet 56 | | Some lastModified -> 57 | let lastModified = lastModified.CutOffMs() 58 | 59 | match 60 | requestHeaders.IfUnmodifiedSince.Value > DateTimeOffset.UtcNow.CutOffMs() 61 | || requestHeaders.IfUnmodifiedSince.Value >= lastModified 62 | with 63 | | true -> AllConditionsMet 64 | | false -> ConditionFailed 65 | 66 | member private this.ValidateIfNoneMatch (eTag: EntityTagHeaderValue option) (requestHeaders: RequestHeaders) = 67 | match isNotNull requestHeaders.IfNoneMatch && requestHeaders.IfNoneMatch.Any() with 68 | | false -> NoConditionsSpecified 69 | | true -> 70 | match eTag with 71 | | None -> AllConditionsMet 72 | | Some eTag -> 73 | requestHeaders.IfNoneMatch 74 | |> Seq.exists (fun t -> t.Compare(eTag, false)) 75 | |> function 76 | | false -> AllConditionsMet 77 | | true -> 78 | match this.IsHeadOrGetRequest() with 79 | | true -> ResourceNotModified 80 | | false -> ConditionFailed 81 | 82 | member private this.ValidateIfModifiedSince (lastModified: DateTimeOffset option) (requestHeaders: RequestHeaders) = 83 | match requestHeaders.IfModifiedSince.HasValue && this.IsHeadOrGetRequest() with 84 | | false -> NoConditionsSpecified 85 | | true -> 86 | match lastModified with 87 | | None -> AllConditionsMet 88 | | Some lastModified -> 89 | let lastModified = lastModified.CutOffMs() 90 | 91 | match 92 | requestHeaders.IfModifiedSince.Value <= DateTimeOffset.UtcNow.CutOffMs() 93 | && requestHeaders.IfModifiedSince.Value < lastModified 94 | with 95 | | true -> AllConditionsMet 96 | | false -> ResourceNotModified 97 | 98 | [] 99 | type PreconditionExtensions() = 100 | /// 101 | /// Validates the following conditional HTTP headers of the HTTP request: 102 | /// 103 | /// If-Match 104 | /// 105 | /// If-None-Match 106 | /// 107 | /// If-Modified-Since 108 | /// 109 | /// If-Unmodified-Since 110 | /// 111 | /// The current http context object. 112 | /// Optional ETag. You can use the static EntityTagHeaderValue.FromString helper method to generate a valid object. 113 | /// Optional object denoting the last modified date. 114 | /// 115 | /// Returns a Precondition union type, which can have one of the following cases: 116 | /// 117 | /// NoConditionsSpecified: No validation has taken place, because the client didn't send any conditional HTTP headers. 118 | /// 119 | /// ConditionFailed: At least one condition couldn't be satisfied. It is advised to return a 412 status code back to the client (you can use the HttpContext.PreconditionFailedResponse() method for that purpose). 120 | /// 121 | /// ResourceNotModified: The resource hasn't changed since the last visit. The server can skip processing this request and return a 304 status code back to the client (you can use the HttpContext.NotModifiedResponse() method for that purpose). 122 | /// 123 | /// AllConditionsMet: All pre-conditions can be satisfied. The server should continue processing the request as normal. 124 | /// 125 | [] 126 | static member ValidatePreconditions 127 | (ctx: HttpContext, eTag: EntityTagHeaderValue option, lastModified: DateTimeOffset option) 128 | = 129 | // Parse headers 130 | let responseHeaders = ctx.Response.GetTypedHeaders() 131 | let requestHeaders = ctx.Request.GetTypedHeaders() 132 | 133 | // Helper bind functions to chain validation functions 134 | let bind (result: RequestHeaders -> Precondition) = 135 | function 136 | | NoConditionsSpecified -> result requestHeaders 137 | | AllConditionsMet -> 138 | match result requestHeaders with 139 | | NoConditionsSpecified -> AllConditionsMet 140 | | AllConditionsMet -> AllConditionsMet 141 | | ConditionFailed -> ConditionFailed 142 | | ResourceNotModified -> ResourceNotModified 143 | | ConditionFailed -> ConditionFailed 144 | | ResourceNotModified -> ResourceNotModified 145 | 146 | let ifNotSpecified (result: RequestHeaders -> Precondition) = 147 | function 148 | | NoConditionsSpecified -> result requestHeaders 149 | | AllConditionsMet -> AllConditionsMet 150 | | ConditionFailed -> ConditionFailed 151 | | ResourceNotModified -> ResourceNotModified 152 | 153 | // Set ETag in the response 154 | eTag |> Option.iter (fun eTagValue -> responseHeaders.ETag <- eTagValue) 155 | 156 | // Set Last-Modified in the response 157 | lastModified 158 | |> Option.iter (fun lastModifiedValue -> responseHeaders.LastModified <- Nullable(lastModifiedValue.CutOffMs())) 159 | 160 | // Validate headers in correct precedence 161 | // RFC: https://tools.ietf.org/html/rfc7232#section-6 162 | requestHeaders 163 | |> ctx.ValidateIfMatch eTag 164 | |> ifNotSpecified (ctx.ValidateIfUnmodifiedSince lastModified) 165 | |> bind (ctx.ValidateIfNoneMatch eTag) 166 | |> ifNotSpecified (ctx.ValidateIfModifiedSince lastModified) 167 | 168 | /// 169 | /// Sends a default HTTP 304 Not Modified response to the client. 170 | /// 171 | /// 172 | [] 173 | static member NotModifiedResponse(ctx: HttpContext) = 174 | ctx.SetStatusCode StatusCodes.Status304NotModified 175 | Some ctx 176 | 177 | /// 178 | /// Sends a default HTTP 412 Precondition Failed response to the client. 179 | /// 180 | /// 181 | [] 182 | static member PreconditionFailedResponse(ctx: HttpContext) = 183 | ctx.SetStatusCode StatusCodes.Status412PreconditionFailed 184 | Some ctx 185 | 186 | // --------------------------- 187 | // HttpHandler functions 188 | // --------------------------- 189 | 190 | /// 191 | /// Validates the following conditional HTTP headers of the request: 192 | /// 193 | /// If-Match 194 | /// 195 | /// If-None-Match 196 | /// 197 | /// If-Modified-Since 198 | /// 199 | /// If-Unmodified-Since 200 | /// 201 | /// 202 | /// If the conditions are met (or non existent) then it will invoke the next http handler in the pipeline otherwise it will return a 304 Not Modified or 412 Precondition Failed response. 203 | /// 204 | /// Optional ETag. You can use the static EntityTagHeaderValue.FromString helper method to generate a valid object. 205 | /// Optional object denoting the last modified date. 206 | /// 207 | /// 208 | /// A Giraffe function which can be composed into a bigger web application. 209 | let validatePreconditions (eTag: EntityTagHeaderValue option) (lastModified: DateTimeOffset option) : HttpHandler = 210 | fun (next: HttpFunc) (ctx: HttpContext) -> 211 | task { 212 | match ctx.ValidatePreconditions(eTag, lastModified) with 213 | | ConditionFailed -> return ctx.PreconditionFailedResponse() 214 | | ResourceNotModified -> return ctx.NotModifiedResponse() 215 | | AllConditionsMet 216 | | NoConditionsSpecified -> return! next ctx 217 | } 218 | -------------------------------------------------------------------------------- /tests/Giraffe.Tests/EndpointRoutingTests.fs: -------------------------------------------------------------------------------- 1 | module Giraffe.Tests.EndpointRoutingTests 2 | 3 | open System 4 | open Microsoft.AspNetCore.Builder 5 | open Microsoft.Extensions.DependencyInjection 6 | open Xunit 7 | open Giraffe 8 | open Giraffe.EndpointRouting 9 | open System.Net.Http 10 | 11 | // --------------------------------- 12 | // routef Tests 13 | // --------------------------------- 14 | 15 | [] 16 | [] 17 | [] 18 | [] 19 | [] // ShortGuid 20 | [] 21 | [] 22 | [] // ShortGuid 23 | [] 24 | [] 25 | [] 26 | let ``routef: GET "/try-a-guid/%O" returns "Success: ..." or "Not Found"`` (potentialGuid: string, expected: string) = 27 | task { 28 | let endpoints: Endpoint list = 29 | [ 30 | GET [ 31 | route "/" (text "Hello World") 32 | route "/foo" (text "bar") 33 | routef "/try-a-guid/%O" (fun (guid: Guid) -> text $"Success: {guid}") 34 | ] 35 | ] 36 | 37 | let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound 38 | 39 | let configureApp (app: IApplicationBuilder) = 40 | app.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler) 41 | 42 | let configureServices (services: IServiceCollection) = 43 | services.AddRouting().AddGiraffe() |> ignore 44 | 45 | let request = createRequest HttpMethod.Get $"/try-a-guid/{potentialGuid}" 46 | 47 | let! response = makeRequest (fun () -> configureApp) configureServices () request 48 | 49 | let! content = response |> readText 50 | 51 | content |> shouldEqual expected 52 | } 53 | 54 | [] 55 | [] 56 | [] 57 | [] 58 | [] 59 | let ``routeWithExtensions: GET request returns expected result`` (path: string, expected: string) = 60 | let endpoints = 61 | [ 62 | GET [ 63 | routeWithExtensions (id) "/" (text "Hello World") 64 | route "/foo" (text "bar") 65 | routeWithExtensions (fun eb -> eb.RequireRateLimiting("nothing")) "/bar" (text "baz") 66 | ] 67 | ] 68 | 69 | let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound 70 | 71 | let configureApp (app: IApplicationBuilder) = 72 | app.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler) 73 | 74 | let configureServices (services: IServiceCollection) = 75 | services.AddRouting().AddGiraffe() |> ignore 76 | 77 | task { 78 | let request = createRequest HttpMethod.Get path 79 | 80 | let! response = makeRequest (fun () -> configureApp) configureServices () request 81 | 82 | let! content = response |> readText 83 | 84 | content |> shouldEqual expected 85 | } 86 | 87 | [] 88 | [] 89 | [] 90 | [] 91 | [] 92 | let ``routefWithExtensions: GET request returns expected result`` (path: string, expected: string) = 93 | let endpoints = 94 | [ 95 | GET [ 96 | routefWithExtensions (id) "/empty/%i" (fun i -> text $"/empty i = {i}") 97 | routef "/normal/%i" (fun i -> text $"/normal i = {i}") 98 | routefWithExtensions (fun eb -> eb.CacheOutput("nothing")) "/cache/%i" (fun i -> text $"/cache i = {i}") 99 | ] 100 | ] 101 | 102 | let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound 103 | 104 | let configureApp (app: IApplicationBuilder) = 105 | app.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler) 106 | 107 | let configureServices (services: IServiceCollection) = 108 | services.AddRouting().AddGiraffe() |> ignore 109 | 110 | task { 111 | let request = createRequest HttpMethod.Get path 112 | 113 | let! response = makeRequest (fun () -> configureApp) configureServices () request 114 | 115 | let! content = response |> readText 116 | 117 | content |> shouldEqual expected 118 | } 119 | 120 | [] 121 | [] 122 | [] 123 | [] 124 | [] 125 | let ``subRouteWithExtensions: GET request returns expected result`` (path: string, expected: string) = 126 | let endpoints = 127 | [ 128 | subRouteWithExtensions (id) "/api" [ 129 | GET [ 130 | routefWithExtensions (id) "/foo/%i" (fun i -> text $"/foo i = {i}") 131 | routef "/bar/%i" (fun i -> text $"/bar i = {i}") 132 | routefWithExtensions (fun eb -> eb.CacheOutput("nothing")) "/baz/%i" (fun i -> text $"/baz i = {i}") 133 | ] 134 | ] 135 | ] 136 | 137 | let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound 138 | 139 | let configureApp (app: IApplicationBuilder) = 140 | app.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler) 141 | 142 | let configureServices (services: IServiceCollection) = 143 | services.AddRouting().AddGiraffe() |> ignore 144 | 145 | task { 146 | let request = createRequest HttpMethod.Get path 147 | 148 | let! response = makeRequest (fun () -> configureApp) configureServices () request 149 | 150 | let! content = response |> readText 151 | 152 | content |> shouldEqual expected 153 | } 154 | 155 | [] 156 | [] 157 | [] 158 | [] 159 | [] 160 | [] 161 | [] 162 | [] 163 | [] 164 | let ``routef: GET "/pet/%i:petId" returns named parameter`` (path: string, expected: string) = 165 | task { 166 | let endpoints: Endpoint list = 167 | [ 168 | GET [ routef "/pet/%i:petId" (fun (petId: int) -> text ($"PetId: {petId}")) ] 169 | GET [ 170 | routef 171 | "/foo/%i/bar/%i:barId" 172 | (fun (fooId: int, barId: int) -> text ($"FooId: {fooId}, BarId: {barId}")) 173 | ] 174 | ] 175 | 176 | let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound 177 | 178 | let configureApp (app: IApplicationBuilder) = 179 | app.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler) 180 | 181 | let configureServices (services: IServiceCollection) = 182 | services.AddRouting().AddGiraffe() |> ignore 183 | 184 | let request = createRequest HttpMethod.Get path 185 | 186 | let! response = makeRequest (fun () -> configureApp) configureServices () request 187 | let! content = response |> readText 188 | content |> shouldEqual expected 189 | } 190 | 191 | [] 192 | [] 193 | [] 194 | [] 195 | [] 196 | let ``routef: GET "/foo/%i:fooId/bar/%i" returns named and unnamed parameters`` (path: string, expected: string) = 197 | task { 198 | let endpoints: Endpoint list = 199 | [ 200 | GET [ 201 | routef 202 | "/foo/%i:fooId/bar/%s:barId" 203 | (fun (fooId: int, barId: string) -> text ($"FooId: {fooId}, BarId: {barId}")) 204 | ] 205 | ] 206 | 207 | let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound 208 | 209 | let configureApp (app: IApplicationBuilder) = 210 | app.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler) 211 | 212 | let configureServices (services: IServiceCollection) = 213 | services.AddRouting().AddGiraffe() |> ignore 214 | 215 | let request = createRequest HttpMethod.Get path 216 | 217 | let! response = makeRequest (fun () -> configureApp) configureServices () request 218 | let! content = response |> readText 219 | content |> shouldEqual expected 220 | } 221 | 222 | [] 223 | [] 224 | [] 225 | [] 226 | [] 227 | let ``routef: GET "/foo/%i:fooId/bar/%i/baz/%s" returns named and unnamed parameters`` 228 | (path: string, expected: string) 229 | = 230 | task { 231 | let endpoints: Endpoint list = 232 | [ 233 | GET [ 234 | routef 235 | "/foo/%i:fooId/bar/%i/baz/%s" 236 | (fun (fooId: int, barId: int, bazName: string) -> 237 | text ($"FooId: {fooId}, BarId: {barId}, BazName: {bazName}") 238 | ) 239 | ] 240 | ] 241 | 242 | let notFoundHandler = "Not Found" |> text |> RequestErrors.notFound 243 | 244 | let configureApp (app: IApplicationBuilder) = 245 | app.UseRouting().UseGiraffe(endpoints).UseGiraffe(notFoundHandler) 246 | 247 | let configureServices (services: IServiceCollection) = 248 | services.AddRouting().AddGiraffe() |> ignore 249 | 250 | let request = createRequest HttpMethod.Get path 251 | 252 | let! response = makeRequest (fun () -> configureApp) configureServices () request 253 | let! content = response |> readText 254 | content |> shouldEqual expected 255 | } 256 | -------------------------------------------------------------------------------- /Giraffe.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5FC3763D-0893-4C44-81EC-84484527D23C}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe", "src\Giraffe\Giraffe.fsproj", "{2B9A54E8-005B-40B3-A734-ED630504E522}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{01222765-17F4-456D-B79E-EC106C20861A}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.Tests", "tests\Giraffe.Tests\Giraffe.Tests.fsproj", "{1B876F13-39D0-4A44-9A63-A768AFB6E17A}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{9E6451FB-26E0-4AE4-A469-847F9602E999}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EndpointRoutingApp", "EndpointRoutingApp", "{B9B26DDC-608C-42FE-9AB9-6CF0EE4920CD}" 17 | EndProject 18 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "EndpointRoutingApp", "samples\EndpointRoutingApp\EndpointRoutingApp.fsproj", "{0E15F922-7A44-4116-9DAB-FAEB94392FEC}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{FADD8661-7ADD-42BA-B3BF-F8C8DB70BE00}" 21 | ProjectSection(SolutionItems) = preProject 22 | SECURITY.md = SECURITY.md 23 | RELEASE_NOTES.md = RELEASE_NOTES.md 24 | README.md = README.md 25 | NuGet.config = NuGet.config 26 | LICENSE = LICENSE 27 | giraffe.png = giraffe.png 28 | CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md 29 | DOCUMENTATION.md = DOCUMENTATION.md 30 | giraffe-64x64.png = giraffe-64x64.png 31 | EndProjectSection 32 | EndProject 33 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ResponseCachingApp", "samples\ResponseCachingApp\ResponseCachingApp.fsproj", "{FA102AC4-4608-42F9-86C1-1472B416A76E}" 34 | EndProject 35 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NewtonsoftJson", "samples\NewtonsoftJson\NewtonsoftJson.fsproj", "{A08230F1-DA24-4059-A7F9-4743B36DD3E9}" 36 | EndProject 37 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "GlobalRateLimiting", "samples\GlobalRateLimiting\GlobalRateLimiting.fsproj", "{C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}" 38 | EndProject 39 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "RateLimiting", "samples\RateLimiting\RateLimiting.fsproj", "{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}" 40 | EndProject 41 | Global 42 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 43 | Debug|Any CPU = Debug|Any CPU 44 | Debug|x64 = Debug|x64 45 | Debug|x86 = Debug|x86 46 | Release|Any CPU = Release|Any CPU 47 | Release|x64 = Release|x64 48 | Release|x86 = Release|x86 49 | EndGlobalSection 50 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 51 | {2B9A54E8-005B-40B3-A734-ED630504E522}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {2B9A54E8-005B-40B3-A734-ED630504E522}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {2B9A54E8-005B-40B3-A734-ED630504E522}.Debug|x64.ActiveCfg = Debug|Any CPU 54 | {2B9A54E8-005B-40B3-A734-ED630504E522}.Debug|x64.Build.0 = Debug|Any CPU 55 | {2B9A54E8-005B-40B3-A734-ED630504E522}.Debug|x86.ActiveCfg = Debug|Any CPU 56 | {2B9A54E8-005B-40B3-A734-ED630504E522}.Debug|x86.Build.0 = Debug|Any CPU 57 | {2B9A54E8-005B-40B3-A734-ED630504E522}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {2B9A54E8-005B-40B3-A734-ED630504E522}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {2B9A54E8-005B-40B3-A734-ED630504E522}.Release|x64.ActiveCfg = Release|Any CPU 60 | {2B9A54E8-005B-40B3-A734-ED630504E522}.Release|x64.Build.0 = Release|Any CPU 61 | {2B9A54E8-005B-40B3-A734-ED630504E522}.Release|x86.ActiveCfg = Release|Any CPU 62 | {2B9A54E8-005B-40B3-A734-ED630504E522}.Release|x86.Build.0 = Release|Any CPU 63 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 64 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A}.Debug|Any CPU.Build.0 = Debug|Any CPU 65 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A}.Debug|x64.ActiveCfg = Debug|Any CPU 66 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A}.Debug|x64.Build.0 = Debug|Any CPU 67 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A}.Debug|x86.ActiveCfg = Debug|Any CPU 68 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A}.Debug|x86.Build.0 = Debug|Any CPU 69 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A}.Release|Any CPU.ActiveCfg = Release|Any CPU 70 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A}.Release|Any CPU.Build.0 = Release|Any CPU 71 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A}.Release|x64.ActiveCfg = Release|Any CPU 72 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A}.Release|x64.Build.0 = Release|Any CPU 73 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A}.Release|x86.ActiveCfg = Release|Any CPU 74 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A}.Release|x86.Build.0 = Release|Any CPU 75 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 76 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC}.Debug|Any CPU.Build.0 = Debug|Any CPU 77 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC}.Debug|x64.ActiveCfg = Debug|Any CPU 78 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC}.Debug|x64.Build.0 = Debug|Any CPU 79 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC}.Debug|x86.ActiveCfg = Debug|Any CPU 80 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC}.Debug|x86.Build.0 = Debug|Any CPU 81 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC}.Release|Any CPU.ActiveCfg = Release|Any CPU 82 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC}.Release|Any CPU.Build.0 = Release|Any CPU 83 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC}.Release|x64.ActiveCfg = Release|Any CPU 84 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC}.Release|x64.Build.0 = Release|Any CPU 85 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC}.Release|x86.ActiveCfg = Release|Any CPU 86 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC}.Release|x86.Build.0 = Release|Any CPU 87 | {FA102AC4-4608-42F9-86C1-1472B416A76E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 88 | {FA102AC4-4608-42F9-86C1-1472B416A76E}.Debug|Any CPU.Build.0 = Debug|Any CPU 89 | {FA102AC4-4608-42F9-86C1-1472B416A76E}.Debug|x64.ActiveCfg = Debug|Any CPU 90 | {FA102AC4-4608-42F9-86C1-1472B416A76E}.Debug|x64.Build.0 = Debug|Any CPU 91 | {FA102AC4-4608-42F9-86C1-1472B416A76E}.Debug|x86.ActiveCfg = Debug|Any CPU 92 | {FA102AC4-4608-42F9-86C1-1472B416A76E}.Debug|x86.Build.0 = Debug|Any CPU 93 | {FA102AC4-4608-42F9-86C1-1472B416A76E}.Release|Any CPU.ActiveCfg = Release|Any CPU 94 | {FA102AC4-4608-42F9-86C1-1472B416A76E}.Release|Any CPU.Build.0 = Release|Any CPU 95 | {FA102AC4-4608-42F9-86C1-1472B416A76E}.Release|x64.ActiveCfg = Release|Any CPU 96 | {FA102AC4-4608-42F9-86C1-1472B416A76E}.Release|x64.Build.0 = Release|Any CPU 97 | {FA102AC4-4608-42F9-86C1-1472B416A76E}.Release|x86.ActiveCfg = Release|Any CPU 98 | {FA102AC4-4608-42F9-86C1-1472B416A76E}.Release|x86.Build.0 = Release|Any CPU 99 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 100 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9}.Debug|Any CPU.Build.0 = Debug|Any CPU 101 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9}.Debug|x64.ActiveCfg = Debug|Any CPU 102 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9}.Debug|x64.Build.0 = Debug|Any CPU 103 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9}.Debug|x86.ActiveCfg = Debug|Any CPU 104 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9}.Debug|x86.Build.0 = Debug|Any CPU 105 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9}.Release|Any CPU.ActiveCfg = Release|Any CPU 106 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9}.Release|Any CPU.Build.0 = Release|Any CPU 107 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9}.Release|x64.ActiveCfg = Release|Any CPU 108 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9}.Release|x64.Build.0 = Release|Any CPU 109 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9}.Release|x86.ActiveCfg = Release|Any CPU 110 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9}.Release|x86.Build.0 = Release|Any CPU 111 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 112 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Debug|Any CPU.Build.0 = Debug|Any CPU 113 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Debug|x64.ActiveCfg = Debug|Any CPU 114 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Debug|x64.Build.0 = Debug|Any CPU 115 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Debug|x86.ActiveCfg = Debug|Any CPU 116 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Debug|x86.Build.0 = Debug|Any CPU 117 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Release|Any CPU.ActiveCfg = Release|Any CPU 118 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Release|Any CPU.Build.0 = Release|Any CPU 119 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Release|x64.ActiveCfg = Release|Any CPU 120 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Release|x64.Build.0 = Release|Any CPU 121 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Release|x86.ActiveCfg = Release|Any CPU 122 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Release|x86.Build.0 = Release|Any CPU 123 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 124 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|Any CPU.Build.0 = Debug|Any CPU 125 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|x64.ActiveCfg = Debug|Any CPU 126 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|x64.Build.0 = Debug|Any CPU 127 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|x86.ActiveCfg = Debug|Any CPU 128 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|x86.Build.0 = Debug|Any CPU 129 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|Any CPU.ActiveCfg = Release|Any CPU 130 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|Any CPU.Build.0 = Release|Any CPU 131 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|x64.ActiveCfg = Release|Any CPU 132 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|x64.Build.0 = Release|Any CPU 133 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|x86.ActiveCfg = Release|Any CPU 134 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|x86.Build.0 = Release|Any CPU 135 | EndGlobalSection 136 | GlobalSection(SolutionProperties) = preSolution 137 | HideSolutionNode = FALSE 138 | EndGlobalSection 139 | GlobalSection(ExtensibilityGlobals) = postSolution 140 | SolutionGuid = {77FFE315-7928-4985-B60D-8009AE1A3805} 141 | EndGlobalSection 142 | GlobalSection(NestedProjects) = preSolution 143 | {2B9A54E8-005B-40B3-A734-ED630504E522} = {5FC3763D-0893-4C44-81EC-84484527D23C} 144 | {1B876F13-39D0-4A44-9A63-A768AFB6E17A} = {01222765-17F4-456D-B79E-EC106C20861A} 145 | {B9B26DDC-608C-42FE-9AB9-6CF0EE4920CD} = {9E6451FB-26E0-4AE4-A469-847F9602E999} 146 | {0E15F922-7A44-4116-9DAB-FAEB94392FEC} = {B9B26DDC-608C-42FE-9AB9-6CF0EE4920CD} 147 | {FA102AC4-4608-42F9-86C1-1472B416A76E} = {9E6451FB-26E0-4AE4-A469-847F9602E999} 148 | {A08230F1-DA24-4059-A7F9-4743B36DD3E9} = {9E6451FB-26E0-4AE4-A469-847F9602E999} 149 | {C5E71E00-4DD0-4ED8-B781-7DB63B7565E4} = {9E6451FB-26E0-4AE4-A469-847F9602E999} 150 | {B6A90A80-FB51-48D6-8273-DA651CE2F3F9} = {9E6451FB-26E0-4AE4-A469-847F9602E999} 151 | EndGlobalSection 152 | EndGlobal 153 | -------------------------------------------------------------------------------- /src/Giraffe/FormatExpressions.fs: -------------------------------------------------------------------------------- 1 | module Giraffe.FormatExpressions 2 | 3 | open System 4 | open System.Text.RegularExpressions 5 | open Microsoft.FSharp.Reflection 6 | open FSharp.Core 7 | 8 | // --------------------------- 9 | // String matching functions 10 | // --------------------------- 11 | 12 | let private formatStringMap = 13 | let decodeSlashes (str: string) = 14 | // Kestrel has made the weird decision to 15 | // partially decode a route argument, which 16 | // means that a given route argument would get 17 | // entirely URL decoded except for '%2F' (/). 18 | // Hence decoding %2F must happen separately as 19 | // part of the string parsing function. 20 | // 21 | // For more information please check: 22 | // https://github.com/aspnet/Mvc/issues/4599 23 | str.Replace("%2F", "/").Replace("%2f", "/") 24 | 25 | let parseGuid (str: string) = 26 | match str.Length with 27 | | 22 -> ShortGuid.toGuid str 28 | | _ -> Guid str 29 | 30 | let guidPattern = 31 | "([0-9A-Fa-f]{8}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{12}|[0-9A-Fa-f]{32}|[-_0-9A-Za-z]{22})" 32 | 33 | let shortIdPattern = "([-_0-9A-Za-z]{10}[048AEIMQUYcgkosw])" 34 | 35 | dict 36 | [ 37 | // Char Regex Parser 38 | // ------------------------------------------------------------- 39 | 'b', ("(?i:(true|false)){1}", (fun (s: string) -> bool.Parse s) >> box) // bool 40 | 'c', ("([^/]{1})", char >> box) // char 41 | 's', ("([^/]+)", decodeSlashes >> box) // string 42 | 'i', ("(-?\d+)", int32 >> box) // int 43 | 'd', ("(-?\d+)", int64 >> box) // int64 44 | 'f', ("(-?\d+\.{1}\d+)", float >> box) // float 45 | 'O', (guidPattern, parseGuid >> box) // Guid 46 | 'u', (shortIdPattern, ShortId.toUInt64 >> box) 47 | ] // uint64 48 | 49 | type MatchMode = 50 | | Exact // Will try to match entire string from start to end. 51 | | StartsWith // Will try to match a substring. Subject string should start with test case. 52 | | EndsWith // Will try to match a substring. Subject string should end with test case. 53 | | Contains // Will try to match a substring. Subject string should contain test case. 54 | 55 | type MatchOptions = 56 | { 57 | IgnoreCase: bool 58 | MatchMode: MatchMode 59 | } 60 | 61 | static member Exact = 62 | { 63 | IgnoreCase = false 64 | MatchMode = Exact 65 | } 66 | 67 | static member IgnoreCaseExact = { IgnoreCase = true; MatchMode = Exact } 68 | 69 | let private convertToRegexPatternAndFormatChars (mode: MatchMode) (formatString: string) = 70 | let rec convert (chars: char list) = 71 | match chars with 72 | | '%' :: '%' :: tail -> 73 | let pattern, formatChars = convert tail 74 | "%" + pattern, formatChars 75 | | '%' :: c :: tail -> 76 | let pattern, formatChars = convert tail 77 | let regex, _ = formatStringMap.[c] 78 | regex + pattern, c :: formatChars 79 | | c :: tail -> 80 | let pattern, formatChars = convert tail 81 | c.ToString() + pattern, formatChars 82 | | [] -> "", [] 83 | 84 | let inline formatRegex mode pattern = 85 | match mode with 86 | | Exact -> "^" + pattern + "$" 87 | | StartsWith -> "^" + pattern 88 | | EndsWith -> pattern + "$" 89 | | Contains -> pattern 90 | 91 | formatString 92 | |> List.ofSeq 93 | |> convert 94 | |> (fun (pattern, formatChars) -> formatRegex mode pattern, formatChars) 95 | 96 | /// 97 | /// Tries to parse an input string based on a given format string and return a tuple of all parsed arguments. 98 | /// 99 | /// The format string which shall be used for parsing. 100 | /// The options record with specifications on how the matching should behave. 101 | /// The input string from which the parsed arguments shall be extracted. 102 | /// Matched value as an option of 'T 103 | let tryMatchInput (format: PrintfFormat<_, _, _, _, 'T>) (options: MatchOptions) (input: string) = 104 | try 105 | let pattern, formatChars = 106 | format.Value 107 | |> Regex.Escape 108 | |> convertToRegexPatternAndFormatChars options.MatchMode 109 | 110 | let options = 111 | match options.IgnoreCase with 112 | | true -> RegexOptions.IgnoreCase 113 | | false -> RegexOptions.None 114 | 115 | let result = Regex.Match(input, pattern, options) 116 | 117 | if result.Groups.Count <= 1 then 118 | None 119 | else 120 | let groups = result.Groups |> Seq.cast |> Seq.skip 1 121 | 122 | let values = 123 | (groups, formatChars) 124 | ||> Seq.map2 (fun g c -> 125 | let _, parser = formatStringMap.[c] 126 | let value = parser g.Value 127 | value 128 | ) 129 | |> Seq.toArray 130 | 131 | let result = 132 | match values.Length with 133 | | 1 -> values.[0] 134 | | _ -> 135 | let types = values |> Array.map (fun v -> v.GetType()) 136 | let tupleType = FSharpType.MakeTupleType types 137 | FSharpValue.MakeTuple(values, tupleType) 138 | 139 | result :?> 'T |> Some 140 | with _ -> 141 | None 142 | 143 | /// 144 | /// Tries to parse an input string based on a given format string and return a tuple of all parsed arguments. 145 | /// 146 | /// The format string which shall be used for parsing. 147 | /// The flag to make matching case insensitive. 148 | /// The input string from which the parsed arguments shall be extracted. 149 | /// Matched value as an option of 'T 150 | let tryMatchInputExact (format: PrintfFormat<_, _, _, _, 'T>) (ignoreCase: bool) (input: string) = 151 | let options = 152 | match ignoreCase with 153 | | true -> MatchOptions.IgnoreCaseExact 154 | | false -> MatchOptions.Exact 155 | 156 | tryMatchInput format options input 157 | 158 | 159 | // --------------------------- 160 | // Validation helper functions 161 | // --------------------------- 162 | 163 | /// **Description** 164 | /// 165 | /// Validates if a given format string can be matched with a given tuple. 166 | /// 167 | /// **Parameters** 168 | /// 169 | /// `format`: The format string which shall be used for parsing. 170 | /// 171 | /// **Output** 172 | /// 173 | /// Returns `unit` if validation was successful otherwise will throw an `Exception`. 174 | /// 175 | /// Validates if a given format string can be matched with a given tuple. 176 | /// 177 | /// The format string which shall be used for parsing. 178 | /// Returns if validation was successful otherwise will throw an `Exception`. 179 | let validateFormat (format: PrintfFormat<_, _, _, _, 'T>) = 180 | 181 | let mapping = 182 | [ 183 | 'b', typeof // bool 184 | 'c', typeof // char 185 | 's', typeof // string 186 | 'i', typeof // int 187 | 'd', typeof // int64 188 | 'f', typeof // float 189 | 'O', typeof // guid 190 | 'u', typeof 191 | ] // guid 192 | 193 | let tuplePrint pos last name = 194 | let mutable result = "(" 195 | 196 | for i in 0..last do 197 | if i = pos then 198 | result <- result + name 199 | else 200 | result <- result + "_" 201 | 202 | if i <> last then 203 | result <- result + "," 204 | 205 | result + ")" 206 | 207 | let posPrint = 208 | function 209 | | 1 -> "1st" 210 | | 2 -> "2nd" 211 | | 3 -> "3rd" 212 | | x -> x.ToString() + "th" 213 | 214 | let t = typeof<'T> 215 | let path = format.Value 216 | let mutable parseChars = [] 217 | let mutable matchNext = false 218 | let mutable matches = 0 219 | 220 | let rec charTypeMatch ls mChar = 221 | match ls with 222 | | [] -> () 223 | | (tChar, x) :: xs -> 224 | if tChar = mChar then 225 | parseChars <- (mChar, x) :: parseChars 226 | matches <- matches + 1 227 | else 228 | charTypeMatch xs mChar 229 | 230 | let rec typeCharMatch ls (xType: System.Type) = 231 | match ls with 232 | | [] -> sprintf "%s has no associated format char parameter" xType.Name 233 | | (tChar, x) :: xs -> 234 | if xType = x then 235 | sprintf "%s uses format char '%%%c'" xType.Name tChar 236 | else 237 | typeCharMatch xs xType 238 | 239 | for i in 0 .. path.Length - 1 do 240 | let mChar = path.[i] 241 | 242 | if matchNext then 243 | charTypeMatch mapping mChar 244 | matchNext <- false 245 | else if mChar = '%' then 246 | matchNext <- true 247 | 248 | if FSharpType.IsTuple(t) then 249 | let types = FSharpType.GetTupleElements(t) 250 | 251 | if types.Length <> matches then 252 | failwithf 253 | "Format string error: Number of parameters (%i) does not match number of tuple variables (%i)." 254 | types.Length 255 | matches 256 | 257 | let rec check (ls, pos) = 258 | match ls, pos with 259 | | [], -1 -> () 260 | | (mChar, ct) :: xs, i -> 261 | if ct <> types.[i] then 262 | let hdlFmt = tuplePrint i (types.Length - 1) types.[i].Name 263 | let expFmt = tuplePrint i (types.Length - 1) ct.Name 264 | let guidance = typeCharMatch mapping types.[i] 265 | 266 | failwithf 267 | "Format string error: routef '%s' has type '%s' but handler expects '%s', mismatch on %s parameter '%%%c', %s." 268 | path 269 | expFmt 270 | hdlFmt 271 | (posPrint (i + 1)) 272 | mChar 273 | guidance 274 | else 275 | check (xs, i - 1) 276 | | x, y -> failwithf "Format string error: Unknown validation error: %A [%i]." x y 277 | 278 | check (parseChars, types.Length - 1) 279 | 280 | else 281 | if matches <> 1 then 282 | failwithf "Format string error: Number of parameters (%i) does not match single variable." matches 283 | 284 | match parseChars with 285 | | [ (mChar, ct) ] -> 286 | if ct <> t then 287 | let guidance = typeCharMatch mapping t 288 | 289 | failwithf 290 | "Format string error: routef '%s' has type '%s' but handler expects '%s', mismatch on parameter '%%%c', %s." 291 | path 292 | ct.Name 293 | t.Name 294 | mChar 295 | guidance 296 | | x -> failwithf "Format string error: Unknown validation error: %A." x 297 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------