├── 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 |
--------------------------------------------------------------------------------