├── .config └── dotnet-tools.json ├── .editorconfig ├── .fantomasignore ├── .github ├── dependabot.yml └── workflows │ ├── build-and-publish-docs.yml │ ├── build-and-test.yml │ ├── dependabot-auto-approve.yml │ └── publish-nuget.yml ├── .gitignore ├── .vscode └── tasks.json ├── Directory.Build.props ├── FsHttp.sln ├── LICENSE ├── README.md ├── artwork └── RestInPeace │ ├── Grayscale Transparent.png │ ├── Original Logo Symbol.png │ ├── Original Logo.png │ ├── Transparent Logo.png │ └── logo_RestInPeace.png ├── build.fsx ├── build.sh ├── docs ├── Composability.fsx ├── Configuration.fsx ├── FSI.fsx ├── HttpClient_Http_Message.fsx ├── Logging.fsx ├── Overview.fsx ├── Release_Notes.fsx ├── Request_Headers.fsx ├── Requesting_FormData.fsx ├── Requesting_Multipart_Files.fsx ├── Response_Handling.fsx ├── Sending_Requests.fsx ├── URLs_and_Query_Params.fsx ├── content │ └── fsdocs-theme.css └── img │ ├── logo.png │ ├── logo_big.png │ └── logo_small.png ├── docu-TODOs.md ├── docu-watch.sh ├── docu.sh ├── fiddle ├── DUShadowingInCEs.fsx ├── assertDslCeMethods.fsx ├── builderPlayground.fsx ├── co-maintainer.fsx ├── debugBuildFiddle.fsx ├── discuss-119-Testing.fsx ├── dotnet_fsi_net5 │ ├── fshttp_with_net5_error.fsx │ ├── fshttp_with_net5_success.fsx │ └── global.json ├── dotnet_fsi_net6 │ ├── fshttp_with_net6_success.fsx │ └── global.json ├── dotnet_fsi_net7 │ ├── global.json │ ├── issue-109-114-fail.fsx │ └── issue-109-114-success.fsx ├── giraffe-issue.fsx ├── issue-101.fsx ├── issue-103.fsx ├── issue-106-AllowFileNameMetadata.fsx ├── issue-109-HeaderFormatException.fsx ├── issue-113.fsx ├── issue-121.fsx ├── issue-126.fsx ├── issue-129.fsx ├── issue-178.fsx ├── pr-110-parsing-MediaTypeHeader.fsx ├── prettyFsiIntegration.fsx ├── pxlClockFsiMessage.fsx ├── readme_and_index_md.fsx ├── scratch.fsx ├── utf8StreamStuff.fsx └── vscode_restclient.fsx ├── global.json ├── publish.sh ├── src ├── FsHttp.FSharpData │ ├── FsHttp.FSharpData.fsproj │ ├── JsonComparison.fs │ ├── JsonExtensions.fs │ └── Response.fs ├── FsHttp.NewtonsoftJson │ ├── FsHttp.NewtonsoftJson.fsproj │ ├── GlobalConfig.fs │ ├── Operators.fs │ └── Response.fs ├── FsHttp │ ├── AssemblyInfo.fs │ ├── Autos.fs │ ├── Defaults.fs │ ├── Domain.fs │ ├── DomainExtensions.fs │ ├── Dsl.CE.fs │ ├── Dsl.CE2.fs │ ├── Dsl.fs │ ├── Extensions.fs │ ├── FsHttp.fsproj │ ├── Fsi.fs │ ├── FsiInit.fs │ ├── GlobalConfig.fs │ ├── Helper.fs │ ├── MimeTypes.fs │ ├── Operators.fs │ ├── Print.fs │ ├── Request.fs │ └── Response.fs ├── Test.CSharp │ ├── BasicTests.cs │ └── Test.CSharp.csproj ├── TestWebServer │ ├── Api │ │ ├── FileCallbackResult.cs │ │ └── TestController.cs │ ├── Program.cs │ ├── Startup.cs │ ├── TestWebServer.csproj │ ├── appsettings.Development.json │ └── appsettings.json └── Tests │ ├── AlternativeSyntaxes.fs │ ├── AssemblyInfo.fs │ ├── Basic.fs │ ├── Body.fs │ ├── BuildersAndSignatures.fs │ ├── Config.fs │ ├── Cookies.fs │ ├── DotNetHttp.fs │ ├── Expectations.fs │ ├── ExtendingBuilders.fs │ ├── Helper.fs │ ├── Helper │ ├── Server.fs │ └── TestHelper.fs │ ├── Json.FSharpData.fs │ ├── Json.NewtonsoftJson.fs │ ├── Json.SystemText.fs │ ├── JsonComparison.fs │ ├── Misc.fs │ ├── Multipart.fs │ ├── Printing.fs │ ├── Proxies.fs │ ├── Resources │ ├── uploadFile.txt │ └── uploadFile2.txt │ ├── Tests.fsproj │ └── UrlsAndQuery.fs └── test.sh /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "fsdocs-tool": { 6 | "version": "20.0.1", 7 | "commands": [ 8 | "fsdocs" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.fs] 2 | indent_size=4 3 | max_line_length=120 4 | fsharp_space_before_parameter=true 5 | fsharp_space_before_lowercase_invocation=true 6 | fsharp_space_before_uppercase_invocation=false 7 | fsharp_space_before_class_constructor=false 8 | fsharp_space_before_member=false 9 | fsharp_space_before_colon=false 10 | fsharp_space_after_comma=true 11 | fsharp_space_before_semicolon=false 12 | fsharp_space_after_semicolon=true 13 | fsharp_space_around_delimiter=true 14 | fsharp_max_if_then_short_width=0 15 | fsharp_max_if_then_else_short_width=60 16 | fsharp_max_infix_operator_expression=80 17 | fsharp_max_record_width=80 18 | fsharp_max_record_number_of_items=1 19 | fsharp_record_multiline_formatter=number_of_items 20 | fsharp_max_array_or_list_width=80 21 | fsharp_max_array_or_list_number_of_items=15 22 | fsharp_array_or_list_multiline_formatter=number_of_items 23 | fsharp_max_value_binding_width=80 24 | fsharp_max_function_binding_width=80 25 | fsharp_max_dot_get_expression_width=80 26 | fsharp_multiline_block_brackets_on_same_column=false 27 | fsharp_newline_between_type_definition_and_members=false 28 | fsharp_align_function_signature_to_indentation=true 29 | fsharp_alternative_long_member_definitions=false 30 | fsharp_multi_line_lambda_closing_newline=true 31 | fsharp_experimental_keep_indent_in_branch=false 32 | fsharp_blank_lines_around_nested_multiline_expressions=true 33 | fsharp_bar_before_discriminated_union_declaration=false 34 | fsharp_multiline_bracket_style=stroustrup 35 | fsharp_newline_before_multiline_computation_expression=true 36 | -------------------------------------------------------------------------------- /.fantomasignore: -------------------------------------------------------------------------------- 1 | # Ignore Fable files 2 | .fable/ 3 | 4 | # Ignore script files 5 | *.fsx 6 | 7 | # ignore files in obj folders 8 | src/**/obj/**/*.fs -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | - package-ecosystem: "nuget" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /.github/workflows/build-and-publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Docs 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | defaults: 13 | run: 14 | working-directory: . 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Setup .NET 8 20 | uses: actions/setup-dotnet@v4 21 | with: 22 | dotnet-version: 8.x 23 | 24 | - name: Build solution 25 | run: dotnet build -c Release 26 | 27 | - name: Make docu.sh executable 28 | run: chmod +x ./docu.sh 29 | 30 | - name: Build docu 31 | run: ./docu.sh 32 | shell: bash 33 | 34 | - name: Upload docs artifact to GH pages 35 | uses: actions/upload-pages-artifact@v3 36 | with: 37 | path: ./.docsOutput 38 | 39 | deploy: 40 | runs-on: ubuntu-latest 41 | 42 | needs: build 43 | 44 | permissions: 45 | pages: write # to deploy to Pages 46 | id-token: write # to verify the deployment originates from an appropriate source 47 | 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | 52 | steps: 53 | - name: Deploy to GitHub Pages 54 | id: deployment 55 | uses: actions/deploy-pages@v4 56 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | defaults: 15 | run: 16 | working-directory: . 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Setup .NET 8 22 | uses: actions/setup-dotnet@v4 23 | with: 24 | dotnet-version: 8.x 25 | 26 | - name: Setup .NET 6 27 | uses: actions/setup-dotnet@v4 28 | with: 29 | dotnet-version: 6.x 30 | 31 | - name: Build and run tests 32 | run: dotnet fsi build.fsx test 33 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-approve.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-approve 2 | on: pull_request 3 | 4 | permissions: 5 | pull-requests: write 6 | 7 | jobs: 8 | dependabot: 9 | runs-on: ubuntu-latest 10 | if: github.actor == 'dependabot[bot]' 11 | steps: 12 | - name: Dependabot metadata 13 | id: metadata 14 | uses: dependabot/fetch-metadata@v2 15 | with: 16 | github-token: '${{ secrets.GITHUB_TOKEN }}' 17 | - name: Approve a PR 18 | run: gh pr review --approve "$PR_URL" 19 | env: 20 | PR_URL: ${{github.event.pull_request.html_url}} 21 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 22 | -------------------------------------------------------------------------------- /.github/workflows/publish-nuget.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Nuget 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | defaults: 10 | run: 11 | working-directory: . 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup .NET 8 17 | uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: 8.x 20 | 21 | - name: nuget publish 22 | env: 23 | nuget_push: ${{ secrets.NUGET_API_KEY }} 24 | run: dotnet fsi build.fsx publish 25 | -------------------------------------------------------------------------------- /.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 | .ionide 7 | symbolCache.db 8 | .paket/load 9 | docs/index.md 10 | output/ 11 | 12 | # User-specific files 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUNIT 46 | *.VisualState.xml 47 | TestResult.xml 48 | 49 | # Build Results of an ATL Project 50 | [Dd]ebugPS/ 51 | [Rr]eleasePS/ 52 | dlldata.c 53 | 54 | # Benchmark Results 55 | BenchmarkDotNet.Artifacts/ 56 | 57 | # .NET Core 58 | project.lock.json 59 | project.fragment.lock.json 60 | artifacts/ 61 | **/Properties/launchSettings.json 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_i.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | *.sbr 81 | *.tlb 82 | *.tli 83 | *.tlh 84 | *.tmp 85 | *.tmp_proj 86 | *.log 87 | *.vspscc 88 | *.vssscc 89 | .builds 90 | *.pidb 91 | *.svclog 92 | *.scc 93 | 94 | # Chutzpah Test files 95 | _Chutzpah* 96 | 97 | # Visual C++ cache files 98 | ipch/ 99 | *.aps 100 | *.ncb 101 | *.opendb 102 | *.opensdf 103 | *.sdf 104 | *.cachefile 105 | *.VC.db 106 | *.VC.VC.opendb 107 | 108 | # Visual Studio profiler 109 | *.psess 110 | *.vsp 111 | *.vspx 112 | *.sap 113 | 114 | # Visual Studio Trace Files 115 | *.e2e 116 | 117 | # TFS 2012 Local Workspace 118 | $tf/ 119 | 120 | # Guidance Automation Toolkit 121 | *.gpState 122 | 123 | # ReSharper is a .NET coding add-in 124 | _ReSharper*/ 125 | *.[Rr]e[Ss]harper 126 | *.DotSettings.user 127 | 128 | # JustCode is a .NET coding add-in 129 | .JustCode 130 | 131 | # TeamCity is a build add-in 132 | _TeamCity* 133 | 134 | # DotCover is a Code Coverage Tool 135 | *.dotCover 136 | 137 | # AxoCover is a Code Coverage Tool 138 | .axoCover/* 139 | !.axoCover/settings.json 140 | 141 | # Visual Studio code coverage results 142 | *.coverage 143 | *.coveragexml 144 | 145 | # NCrunch 146 | _NCrunch_* 147 | .*crunch*.local.xml 148 | nCrunchTemp_* 149 | 150 | # MightyMoose 151 | *.mm.* 152 | AutoTest.Net/ 153 | 154 | # Web workbench (sass) 155 | .sass-cache/ 156 | 157 | # Installshield output folder 158 | [Ee]xpress/ 159 | 160 | # DocProject is a documentation generator add-in 161 | DocProject/buildhelp/ 162 | DocProject/Help/*.HxT 163 | DocProject/Help/*.HxC 164 | DocProject/Help/*.hhc 165 | DocProject/Help/*.hhk 166 | DocProject/Help/*.hhp 167 | DocProject/Help/Html2 168 | DocProject/Help/html 169 | 170 | # Click-Once directory 171 | publish/ 172 | 173 | # Publish Web Output 174 | *.[Pp]ublish.xml 175 | *.azurePubxml 176 | # Note: Comment the next line if you want to checkin your web deploy settings, 177 | # but database connection strings (with potential passwords) will be unencrypted 178 | *.pubxml 179 | *.publishproj 180 | 181 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 182 | # checkin your Azure Web App publish settings, but sensitive information contained 183 | # in these scripts will be unencrypted 184 | PublishScripts/ 185 | 186 | # NuGet Packages 187 | *.nupkg 188 | # The packages folder can be ignored because of Package Restore 189 | **/[Pp]ackages/* 190 | # except build/, which is used as an MSBuild target. 191 | !**/[Pp]ackages/build/ 192 | # Uncomment if necessary however generally it will be regenerated when needed 193 | #!**/[Pp]ackages/repositories.config 194 | # NuGet v3's project.json files produces more ignorable files 195 | *.nuget.props 196 | *.nuget.targets 197 | 198 | # Microsoft Azure Build Output 199 | csx/ 200 | *.build.csdef 201 | 202 | # Microsoft Azure Emulator 203 | ecf/ 204 | rcf/ 205 | 206 | # Windows Store app package directories and files 207 | AppPackages/ 208 | BundleArtifacts/ 209 | Package.StoreAssociation.xml 210 | _pkginfo.txt 211 | *.appx 212 | 213 | # Visual Studio cache files 214 | # files ending in .cache can be ignored 215 | *.[Cc]ache 216 | # but keep track of directories ending in .cache 217 | !*.[Cc]ache/ 218 | 219 | # Others 220 | ClientBin/ 221 | ~$* 222 | *~ 223 | *.dbmdl 224 | *.dbproj.schemaview 225 | *.jfm 226 | *.pfx 227 | *.publishsettings 228 | orleans.codegen.cs 229 | 230 | # Including strong name files can present a security risk 231 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 232 | #*.snk 233 | 234 | # Since there are multiple workflows, uncomment next line to ignore bower_components 235 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 236 | #bower_components/ 237 | 238 | # RIA/Silverlight projects 239 | Generated_Code/ 240 | 241 | # Backup & report files from converting an old project file 242 | # to a newer Visual Studio version. Backup files are not needed, 243 | # because we have git ;-) 244 | _UpgradeReport_Files/ 245 | Backup*/ 246 | UpgradeLog*.XML 247 | UpgradeLog*.htm 248 | ServiceFabricBackup/ 249 | *.rptproj.bak 250 | 251 | # SQL Server files 252 | *.mdf 253 | *.ldf 254 | *.ndf 255 | 256 | # Business Intelligence projects 257 | *.rdl.data 258 | *.bim.layout 259 | *.bim_*.settings 260 | *.rptproj.rsuser 261 | 262 | # Microsoft Fakes 263 | FakesAssemblies/ 264 | 265 | # GhostDoc plugin setting file 266 | *.GhostDoc.xml 267 | 268 | # Node.js Tools for Visual Studio 269 | .ntvs_analysis.dat 270 | node_modules/ 271 | 272 | # Visual Studio 6 build log 273 | *.plg 274 | 275 | # Visual Studio 6 workspace options file 276 | *.opt 277 | 278 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 279 | *.vbw 280 | 281 | # Visual Studio LightSwitch build output 282 | **/*.HTMLClient/GeneratedArtifacts 283 | **/*.DesktopClient/GeneratedArtifacts 284 | **/*.DesktopClient/ModelManifest.xml 285 | **/*.Server/GeneratedArtifacts 286 | **/*.Server/ModelManifest.xml 287 | _Pvt_Extensions 288 | 289 | # Paket dependency manager 290 | .paket/paket.exe 291 | paket-files/ 292 | 293 | # nuget packages 294 | .pack 295 | 296 | # FAKE - F# Make 297 | .fake/ 298 | 299 | # JetBrains Rider 300 | .idea/ 301 | *.sln.iml 302 | 303 | # CodeRush 304 | .cr/ 305 | 306 | # Python Tools for Visual Studio (PTVS) 307 | __pycache__/ 308 | *.pyc 309 | 310 | # Cake - Uncomment if you are using it 311 | # tools/** 312 | # !tools/packages.config 313 | 314 | # Tabs Studio 315 | *.tss 316 | 317 | # Telerik's JustMock configuration file 318 | *.jmconfig 319 | 320 | # BizTalk build output 321 | *.btp.cs 322 | *.btm.cs 323 | *.odx.cs 324 | *.xsd.cs 325 | 326 | # OpenCover UI analysis results 327 | OpenCover/ 328 | 329 | # Azure Stream Analytics local run output 330 | ASALocalRun/ 331 | 332 | # MSBuild Binary and Structured Log 333 | *.binlog 334 | 335 | # NVidia Nsight GPU debugger configuration file 336 | *.nvuser 337 | 338 | # MFractors (Xamarin productivity tool) working folder 339 | .mfractor/ 340 | 341 | # fsdocs 342 | tmp/ 343 | .docsOutput/ 344 | .fsdocs/ -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "options": { 4 | "cwd": "." 5 | }, 6 | "windows": { 7 | "options": { 8 | "shell": { 9 | "executable": "C:\\Program Files\\Git\\bin\\bash.exe", 10 | "args": ["-c"] 11 | } 12 | } 13 | }, 14 | "tasks": [ 15 | { 16 | "label": "FsHttp: Build Solution", 17 | "type": "shell", 18 | "command": "./build.sh", 19 | "group": { 20 | "kind": "build", 21 | "isDefault": false 22 | } 23 | }, 24 | { 25 | "label": "FsHttp: Test Solution", 26 | "type": "shell", 27 | "command": "./test.sh", 28 | "group": { 29 | "kind": "build", 30 | "isDefault": false 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15.0.1 5 | 6 | Ronald Schlenker 7 | Copyright 2024 Ronald Schlenker 8 | http rest HttpClient fetch curl f# c# fSharp 9 | true 10 | logo_small.png 11 | Apache-2.0 12 | README.md 13 | https://github.com/fsprojects/FsHttp 14 | 15 | https://github.com/fsprojects/FsHttp/blob/master/LICENSE 16 | https://www.nuget.org/packages/FsHttp#release-body-tab 17 | 18 | 19 | 20 | ************************************************************** 21 | 22 | +---------+ 23 | | | PXL-JAM 2024 24 | | PXL | - github.com/CuminAndPotato/PXL-JAM 25 | | CLOCK | - WIN a PXL-Clock MK1 26 | | | - until 8th of January 2025 27 | +---------+ 28 | 29 | ************************************************************** 30 | 31 | 32 | 15.0.1 33 | - Added PXK-Clock Promo Message on first FSI evaluation (when logs are enabled) 34 | 35 | 15.0.0 36 | - Added 'enumerable' and 'enumerablePart' as body content functions 37 | - Removed Utf8StringBufferingStream 38 | 39 | 14.5.2 40 | - Added PXK-Clock Promo Message on first FSI evaluation (when logs are enabled) 41 | 42 | 14.5.1 43 | - Fixed untracked bug: using config_useBaseUrl as http template won't crash when printing unfinished requests 44 | 45 | 14.5.0 46 | - Added 'useBaseUrl' and 'transformUrl' to Config for better composability 47 | - Fixed some extension methods 48 | 49 | 14.4.2 50 | - Thanks @bartelink 51 | Pinned FSharp.Core to 5.0.2 in all library projects 52 | Removed net7.0, net8.0 TFM-specific builds 53 | Lots of other cool stability-oriented stuff and detail-work 54 | 55 | 14.4.1 56 | - Fixed missing explicit dependency on FSharp.Core 57 | 58 | 14.4.0 59 | - Fixed pre-configured requests 60 | 61 | 14.3.0 62 | - Added `GetList` JsonElement extension 63 | 64 | 14.2.0 65 | - (Breaking change) Separated Config and PrintHint (...and many more things in these domains) 66 | 67 | 14.1.0 68 | - (Breaking change) Renamed `Extensions.To...Enumerable` to `Extensions.To...Seq` 69 | - Added `toJsonList...` overloads 70 | 71 | 14.0.0 72 | - (Breaking change) Renamed types in Domain: 73 | BodyContent -> SinglepartContent 74 | RequestContent -> BodyContent 75 | FsHttpUrl -> FsHttpTarget 76 | - (Breaking change) FsHttpUrl (now FsHttpTarget) and Header restructured: method, address and queryParams are now part of the FsHttpTarget type. 77 | - Added `headerTransformers` to Config for better composability 78 | 79 | 13.3.0 80 | - (Breaking change) All `Response._TAsync` functions (task based) in F# require a CancellationToken now. 81 | - (Breaking change) Extension methods rework 82 | - (Breaking change) There's no more StartingContext, which means: 83 | we give up a little bit of safety here, for the sake of pre-configuring HTTP requests 84 | without specifying the URL. This is a trade-off we are willing to take. 85 | 86 | 12.2.0 87 | - added HttpMethods for better composability 88 | 89 | 12.1.0 90 | - net8.0 91 | 92 | 12.0.0 93 | - #137 / #102: Change the type for FsHttpUrl.additionalQueryParams from obj to string 94 | - Removed (auto opened) Async.await and Task.map/await 95 | - Moved (auto opened) Async.map to FsHttp.Helper.Async.map 96 | 97 | 11.1.0 98 | - #130 / #105: Add method for user-supplied cancellation token 99 | 100 | 11.0.0 101 | - #121 (Breaking change): Turning off debug logs in FSI (breaking change in signature / namespace) 102 | - #124: Support Repeating Query Parameters (thanks @DaveJohnson8080) 103 | - #106 (Breaking change): Allow filename metadata with other "parts" (thanks @dawedawe) 104 | - Breaking change: ContentTypeForPart custom operations should come after part definition 105 | - #104 (Breaking change): Automatic GZip response decompression per Default 106 | - Other breaking changes: 107 | - Removed `ContentTypeWithEncoding` and used optional `charset` parameter in `ContentType` overloads. 108 | - Renamed `byteArray` to `binary` in Dsl, DslCE and CSharp. 109 | - Caution (!!): Renamed `stringPart` to `textPart` and changed argument order for `name` and `value` in Dsl and DslCE. 110 | - Restructured types in Domain 111 | - `Helper` is a module instead of a namespace, and some things were moved. 112 | - All transformers in config are a list of transformers instead of a single item. 113 | - Removed `setHttpClient`. Please use `setHttpClientFactory` instead. 114 | - `setHttpClientFactory` takes a `Config` as input argument. 115 | 116 | ----------------------------- 117 | -- Old release notes below -- 118 | ----------------------------- 119 | 120 | 7.0.0 121 | - #92: `expect` and `assert` pass through the original response instead of unit. 122 | 123 | 8.0.0 124 | - #93 (thanks @drhumlen): Changed content type 'text/json' to 'application/json'. 125 | - Http modules are always AutoOpen in both Dsl and DslCE. 126 | - No extra modules for builder methods. 127 | 128 | 8.0.1 129 | - #89: No more blocking requests using net5 with FSI. 130 | 131 | 9.0.0 / 9.0.1 132 | - Redefined builders (see README.md). 133 | - Many breaking changes (see "Migrations" sections in the docu). 134 | 135 | 9.0.2 136 | - Added JSON toArray functions 137 | - Fixed #99: Response.saveFile should create the directory if not present. 138 | 139 | 9.0.3 140 | - Supporting netstandard2.1 again. 141 | 142 | 9.0.4 143 | - Referenced lowest possible FSharp.Core and other referenced packages version. 144 | 145 | 9.0.5 146 | - Support for netstandard2.0. 147 | - New 'FsHttp.NewtonsoftJson' integration package. 148 | - More JSON functions and defaults config. 149 | 150 | 9.0.6 151 | - #100 - Removed FSI print messages. 152 | 153 | 9.1.0 154 | - Fixed naming inconsistency for 'Response.deserialize...' functions. 155 | - More C# JSON functions. 156 | 157 | 9.1.1 158 | - Fix: Using GlobalConfig.Json.defaultJsonSerializerOptions as default for jsonSerialize. 159 | 160 | 9.1.2 161 | - Fixed #103: FSI response printing and initialization doesn't work out of the box anymore. 162 | 163 | 10.0.0 164 | - .Net 7 support (thank you @Samuel-Dufour) 165 | - Breaking change: Corrected typo "guessMineTypeFromPath" -> "guessMimeTypeFromPath" 166 | - Breaking change: Module 'Helper', 'HelperInternal' and 'HelperAutos' refactored 167 | - #115: Remove print messages when downloading streams 168 | - Printing: Separate print functions for response and request via Request.print and Response.print 169 | - Printing: Default request (IToRequest) printing in FSI 170 | - Removed net5.0 targets in all projects 171 | - PrintHint.printDebugMessages: Moved to FsHttp.Helper.Fsi.logDebugMessages as a global switch 172 | - #113 - Config.timeoutInSeconds bug 173 | 174 | 10.1.0 175 | - #117: Escape string for query params values (by @maciej-izak - thank you) 176 | (!!) This can be seen as breaking change. 177 | - #112: Allow to add (multiple) headers (by @Samuel-Dufour - thank you) 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /FsHttp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32014.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsHttp", "src\FsHttp\FsHttp.fsproj", "{D1AD2123-678B-4DC9-9DBA-3D6FF2E30F61}" 7 | EndProject 8 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsHttp.FSharpData", "src\FsHttp.FSharpData\FsHttp.FSharpData.fsproj", "{07320ED5-9E4A-452E-80C3-007C3303735F}" 9 | EndProject 10 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsHttp.NewtonsoftJson", "src\FsHttp.NewtonsoftJson\FsHttp.NewtonsoftJson.fsproj", "{CB577E4F-F4F0-4ADA-BD90-036B5ECF17AD}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.CSharp", "src\Test.CSharp\Test.CSharp.csproj", "{1163EF7E-1845-4D2A-8C3F-8691618500DF}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestWebServer", "src\TestWebServer\TestWebServer.csproj", "{E7E326FD-1D66-4DBD-AB60-D006D3B77244}" 15 | EndProject 16 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Tests", "src\Tests\Tests.fsproj", "{63A6AF47-CB3F-4DBA-A16E-B66D6EFA3573}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {D1AD2123-678B-4DC9-9DBA-3D6FF2E30F61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {D1AD2123-678B-4DC9-9DBA-3D6FF2E30F61}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {D1AD2123-678B-4DC9-9DBA-3D6FF2E30F61}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {D1AD2123-678B-4DC9-9DBA-3D6FF2E30F61}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {07320ED5-9E4A-452E-80C3-007C3303735F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {07320ED5-9E4A-452E-80C3-007C3303735F}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {07320ED5-9E4A-452E-80C3-007C3303735F}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {07320ED5-9E4A-452E-80C3-007C3303735F}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {CB577E4F-F4F0-4ADA-BD90-036B5ECF17AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {CB577E4F-F4F0-4ADA-BD90-036B5ECF17AD}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {CB577E4F-F4F0-4ADA-BD90-036B5ECF17AD}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {CB577E4F-F4F0-4ADA-BD90-036B5ECF17AD}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {1163EF7E-1845-4D2A-8C3F-8691618500DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {1163EF7E-1845-4D2A-8C3F-8691618500DF}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {1163EF7E-1845-4D2A-8C3F-8691618500DF}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {1163EF7E-1845-4D2A-8C3F-8691618500DF}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {E7E326FD-1D66-4DBD-AB60-D006D3B77244}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {E7E326FD-1D66-4DBD-AB60-D006D3B77244}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {E7E326FD-1D66-4DBD-AB60-D006D3B77244}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {E7E326FD-1D66-4DBD-AB60-D006D3B77244}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {63A6AF47-CB3F-4DBA-A16E-B66D6EFA3573}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {63A6AF47-CB3F-4DBA-A16E-B66D6EFA3573}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {63A6AF47-CB3F-4DBA-A16E-B66D6EFA3573}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {63A6AF47-CB3F-4DBA-A16E-B66D6EFA3573}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {490D0829-D4E3-4267-8132-14A5B0C9D347} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FsHttp 2 | 3 | [![Build & Tests](https://github.com/schlenkr/FsHttp/actions/workflows/build-and-test.yml/badge.svg?branch=master)](https://github.com/schlenkr/FsHttp/actions/workflows/build-and-test.yml) 4 | [![NuGet](https://img.shields.io/nuget/v/FsHttp.svg?style=flat-square&logo=nuget)](https://www.nuget.org/packages/FsHttp) 5 | [![NuGet Downloads](https://img.shields.io/nuget/dt/FsHttp.svg?style=flat-square)](https://www.nuget.org/packages/FsHttp) 6 | 7 | logo 8 | 9 | FsHttp is a "hackable HTTP client" that offers a legible style for the basics while still affording full access to the underlying HTTP representations for covering unusual cases. It's the best of both worlds: **Convenience and Flexibility**. 10 | 11 | * Use it as a replacement for `.http` files, *VSCode's REST client*, *Postman*, and other tools as an **interactive and programmable playground** for HTTP requests. 12 | * Usable as a **production-ready HTTP client** for applications powered by .NET (C#, VB, F#). 13 | 14 | 👍 Postman? ❤️ FsHttp! https://youtu.be/F508wQu7ET0 15 | 16 | --- 17 | 18 | ## FsHttp ❤️ PXL-Clock 19 | 20 | Allow us a bit of advertising for our PXL-Clock! It's a fun device, made with ❤️ - and it's programmable almost as easy as you write requests with FsHttp :) 21 | 22 |

23 | image 24 |

25 | 26 | Find out more info on the [PXL-Clock Discord Server](https://discord.gg/KDbVdKQh5j) or check out the [PXL-Clock Repo on GitHub](https://github.com/CuminAndPotato/PXL-Clock). 27 | 28 |

29 |

Join the PXL-Clock Community on Discord

30 | 31 | Join Our Discord 32 | 33 |

34 | 35 | --- 36 | 37 | ## Documentation 38 | 39 | * 📖 Please see [FsHttp Documentation](https://fsprojects.github.io/FsHttp) site for detailed documentation. 40 | * 🧪 In addition, have a look at the [Integration Tests](https://github.com/schlenkr/FsHttp/tree/master/src/Tests) that show various library details. 41 | 42 | ### F# syntax example 43 | 44 | ```fsharp 45 | #r "nuget: FsHttp" 46 | 47 | open FsHttp 48 | 49 | http { 50 | POST "https://reqres.in/api/users" 51 | CacheControl "no-cache" 52 | body 53 | jsonSerialize 54 | {| 55 | name = "morpheus" 56 | job = "leader" 57 | |} 58 | } 59 | |> Request.send 60 | ``` 61 | 62 | ### C# syntax example 63 | 64 | ```csharp 65 | #r "nuget: FsHttp" 66 | 67 | using FsHttp; 68 | 69 | await Http 70 | .Post("https://reqres.in/api/users") 71 | .CacheControl("no-cache") 72 | .Body() 73 | .JsonSerialize(new 74 | { 75 | name = "morpheus", 76 | job = "leader" 77 | } 78 | ) 79 | .SendAsync(); 80 | ``` 81 | 82 | ### Release Notes / Migrating to new versions 83 | 84 | * See https://www.nuget.org/packages/FsHttp#release-body-tab 85 | * For different upgrade paths, please read the [Migrations docs section](https://schlenkr.github.io/FsHttp/Release_Notes.html). 86 | 87 | ## Building 88 | 89 | **.Net SDK:** 90 | 91 | You need to have a recent .NET SDK installed, which is specified in `./global.json`. 92 | 93 | **Build Tasks** 94 | 95 | There is a F# build script (`./build.fsx`) that can be used to perform several build tasks from command line. 96 | 97 | For common tasks, there are bash scripts located in the repo root: 98 | 99 | * `./test.sh`: Runs all tests (sources in `./src/Tests`). 100 | * You can pass args to this task. E.g. for executing only some tests: 101 | `./test.sh --filter Name~'Response Decompression'` 102 | * `./docu.sh`: Rebuilds the FsHttp documentation site (sources in `./src/docs`). 103 | * `./docu-watch.sh`: Run it if you are working on the documentation sources, and want to see the result in a browser. 104 | * `./publish.sh`: Publishes all packages (FsHttp and it's integration packages for Newtonsoft and FSharp.Data) to NuGet. 105 | * Always have a look at `./src/Directory.Build.props` and keep the file up-to-date. 106 | 107 | ## Credits 108 | 109 | * Parts of the code were taken from the [HTTP utilities of FSharp.Data](https://fsprojects.github.io/FSharp.Data/library/Http.html). 110 | * Credits to all critics, supporters, contributors, promoters, users, and friends. 111 | -------------------------------------------------------------------------------- /artwork/RestInPeace/Grayscale Transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FsHttp/cd7e362bba6dfd1e1cdb3d357a11dc8468ea8d7b/artwork/RestInPeace/Grayscale Transparent.png -------------------------------------------------------------------------------- /artwork/RestInPeace/Original Logo Symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FsHttp/cd7e362bba6dfd1e1cdb3d357a11dc8468ea8d7b/artwork/RestInPeace/Original Logo Symbol.png -------------------------------------------------------------------------------- /artwork/RestInPeace/Original Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FsHttp/cd7e362bba6dfd1e1cdb3d357a11dc8468ea8d7b/artwork/RestInPeace/Original Logo.png -------------------------------------------------------------------------------- /artwork/RestInPeace/Transparent Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FsHttp/cd7e362bba6dfd1e1cdb3d357a11dc8468ea8d7b/artwork/RestInPeace/Transparent Logo.png -------------------------------------------------------------------------------- /artwork/RestInPeace/logo_RestInPeace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FsHttp/cd7e362bba6dfd1e1cdb3d357a11dc8468ea8d7b/artwork/RestInPeace/logo_RestInPeace.png -------------------------------------------------------------------------------- /build.fsx: -------------------------------------------------------------------------------- 1 | System.Environment.CurrentDirectory <- __SOURCE_DIRECTORY__ 2 | 3 | #r "nuget: Fake.Core.Process" 4 | #r "nuget: Fake.IO.FileSystem" 5 | 6 | open System 7 | 8 | open Fake.Core 9 | open Fake.IO 10 | open Fake.IO.Globbing.Operators 11 | 12 | Trace.trace $"Starting script..." 13 | 14 | module Properties = 15 | let nugetServer = "https://api.nuget.org/v3/index.json" 16 | let nugetPushEnvVarName = "nuget_fshttp" 17 | 18 | [] 19 | module Helper = 20 | 21 | let private runTarget (x: string * _) = 22 | let name,f = x 23 | Trace.trace $"Running task: {name}" 24 | f () 25 | 26 | let run targets = 27 | for t in targets do 28 | runTarget t 29 | 30 | type Args() = 31 | let strippedArgs = 32 | fsi.CommandLineArgs 33 | |> Array.skipWhile (fun x -> x <> __SOURCE_FILE__ ) 34 | |> Array.skip 1 35 | |> Array.toList 36 | let taskName,taskArgs = 37 | match strippedArgs with 38 | | taskName :: taskArgs -> taskName, taskArgs 39 | | _ -> 40 | let msg = $"Wrong args. Expected: fsi :: taskName :: taskArgs" 41 | printfn "%s" msg 42 | Environment.Exit -1 43 | failwith msg 44 | do 45 | printfn $"Task name: {taskName}" 46 | printfn $"Task args: {taskArgs}" 47 | member _.IsTask(arg) = 48 | let res = taskName = arg 49 | printfn $"Checking task '{arg}'... {res} (taskName: '{taskName}')" 50 | res 51 | member _.TaskArgs = taskArgs 52 | 53 | let args = Args() 54 | 55 | type Shell with 56 | static member ExecSuccess (cmd: string, ?arg: string) = 57 | let args = arg |> Option.defaultValue "" |> fun x -> [| x; yield! args.TaskArgs |] |> String.concat " " 58 | printfn $"Executing command '{cmd}' with args: {args}" 59 | let res = Shell.Exec(cmd, ?args = Some args) 60 | if res <> 0 then failwith $"Shell execute was not successful: {res}" else () 61 | 62 | let shallBuild = args.IsTask("build") 63 | let shallTest = args.IsTask("test") 64 | let shallPublish = args.IsTask("publish") 65 | let shallPack = args.IsTask("pack") 66 | 67 | let toolRestore = "toolRestore", fun () -> 68 | Shell.ExecSuccess ("dotnet", "tool restore") 69 | 70 | let clean = "clean", fun () -> 71 | !! "src/**/bin" 72 | ++ "src/**/obj" 73 | ++ ".pack" 74 | |> Shell.cleanDirs 75 | 76 | let slnPath = "./FsHttp.sln" 77 | let build = "build", fun () -> 78 | Shell.ExecSuccess ("dotnet", $"build {slnPath}") 79 | 80 | 81 | let CsTestProjectPath = "./src/Test.CSharp/Test.CSharp.csproj" 82 | let FsharpTestProjectPath = "./src/Tests/Tests.fsproj" 83 | let test = "test", fun () -> 84 | Shell.ExecSuccess ("dotnet", $"test {FsharpTestProjectPath}") 85 | Shell.ExecSuccess ("dotnet", $"test {CsTestProjectPath}") 86 | 87 | let pack = "pack", fun () -> 88 | !! "src/**/FsHttp*.fsproj" 89 | |> Seq.iter (fun p -> 90 | Trace.trace $"SourceDir is: {__SOURCE_DIRECTORY__}" 91 | Shell.ExecSuccess ("dotnet", sprintf "pack %s -o %s -c Release" p (Path.combine __SOURCE_DIRECTORY__ ".pack")) 92 | ) 93 | 94 | // TODO: git tag + release 95 | let publish = "publish", fun () -> 96 | let nugetApiKey = Environment.environVar Properties.nugetPushEnvVarName 97 | !! ".pack/*.nupkg" 98 | |> Seq.iter (fun p -> 99 | Shell.ExecSuccess ("dotnet", $"nuget push {p} -k {nugetApiKey} -s {Properties.nugetServer} --skip-duplicate") 100 | ) 101 | 102 | run [ 103 | clean 104 | toolRestore 105 | 106 | if shallBuild then 107 | build 108 | if shallTest then 109 | test 110 | if shallPack then 111 | pack 112 | if shallPublish then 113 | build 114 | pack 115 | publish 116 | ] 117 | 118 | Trace.trace $"Finished script..." 119 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | dotnet build -------------------------------------------------------------------------------- /docs/Composability.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: Composability 4 | category: Documentation 5 | categoryindex: 1 6 | index: 15 7 | --- 8 | *) 9 | 10 | (*** condition: prepare ***) 11 | #nowarn "211" 12 | #r "../src/FsHttp/bin/Release/net6.0/FsHttp.dll" 13 | open FsHttp 14 | 15 | 16 | (** 17 | ## Composability 18 | 19 | There are many ways to compose HTTP requests with FsHttp, depending on your needs. A common pattern is to define a base request builder that is then used to create more specific request builders. This is useful when targeting different environments, and don't want to repeat the same configuration for each request. 20 | 21 | An example with comments: 22 | *) 23 | 24 | open FsHttp.Operators 25 | 26 | let httpForMySpecialEnvironment = 27 | let baseUrl = "http://my-special-environment" 28 | http { 29 | // we would like to have a fixed URL prefix for all requests. 30 | // So we define a new builder that actually uses a base url, like so: 31 | config_useBaseUrl baseUrl 32 | 33 | // ...in case you need more control, you can also transform the URL: 34 | config_transformUrl (fun url -> baseUrl url) 35 | 36 | // ...or you can transform the header in a similar way. 37 | // Since the description of method is a special thing, 38 | // we have to change the URL for any method using a header transformer, 39 | // like so: 40 | config_transformHeader (fun (header: Header) -> 41 | let address = baseUrl (header.target.address |> Option.defaultValue "") 42 | { header with target.address = Some address }) 43 | 44 | // other header values can be just configured as usual: 45 | AuthorizationBearer "**************" 46 | } 47 | 48 | let response = 49 | httpForMySpecialEnvironment { 50 | GET "/api/v1/users" 51 | } 52 | |> Request.sendAsync 53 | -------------------------------------------------------------------------------- /docs/Configuration.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: Configuration 4 | category: Documentation 5 | categoryindex: 1 6 | index: 12 7 | --- 8 | *) 9 | 10 | (*** condition: prepare ***) 11 | #nowarn "211" 12 | #r "../src/FsHttp/bin/Release/net6.0/FsHttp.dll" 13 | open FsHttp 14 | 15 | 16 | (** 17 | ## Per request configuration 18 | 19 | It's possible to configure requests per instance by the use of `config_` 20 | methods in any stage of the request definition: 21 | *) 22 | http { 23 | config_timeoutInSeconds 11.1 24 | GET "http://myService" 25 | } 26 | 27 | // or 28 | 29 | get "http://myService" 30 | |> Config.timeoutInSeconds 11.1 31 | 32 | (** 33 | ## Global configuration 34 | 35 | You can also set config values globally (inherited when requests are created): 36 | *) 37 | GlobalConfig.defaults 38 | |> Config.timeoutInSeconds 11.1 39 | |> GlobalConfig.set 40 | -------------------------------------------------------------------------------- /docs/FSI.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: F# Interactive Usage 4 | category: Documentation 5 | categoryindex: 1 6 | index: 9 7 | --- 8 | *) 9 | 10 | (*** condition: prepare ***) 11 | #nowarn "211" 12 | #r "../src/FsHttp/bin/Release/net6.0/FsHttp.dll" 13 | open FsHttp 14 | 15 | 16 | (** 17 | ## FSI Setup 18 | *) 19 | 20 | #r @"nuget: FsHttp" 21 | open FsHttp 22 | 23 | 24 | (** 25 | ## FSI Request/Response Formatting 26 | 27 | When you work in FSI, you can control the output formatting with special keywords. 28 | 29 | Some predefined printers are defined in ```./src/FsHttp/DslCE.fs, module Fsi``` 30 | 31 | *) 32 | 33 | http { 34 | GET "https://reqres.in/api/users" 35 | CacheControl "no-cache" 36 | print_withResponseBodyExpanded 37 | } 38 | -------------------------------------------------------------------------------- /docs/HttpClient_Http_Message.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: HttpClient And HttpMessage 4 | category: Documentation 5 | categoryindex: 1 6 | index: 11 7 | --- 8 | *) 9 | 10 | (*** condition: prepare ***) 11 | #nowarn "211" 12 | #r "../src/FsHttp/bin/Release/net6.0/FsHttp.dll" 13 | open FsHttp 14 | 15 | 16 | (** 17 | ## Access HttpClient and HttpMessage 18 | 19 | Transform underlying http client and do whatever you feel you gave to do: 20 | *) 21 | http { 22 | GET @"https://reqres.in/api/users?page=2&delay=3" 23 | config_transformHttpClient (fun httpClient -> 24 | // this will cause a timeout exception 25 | httpClient.Timeout <- System.TimeSpan.FromMilliseconds 1.0 26 | httpClient) 27 | } 28 | 29 | (** 30 | Transform underlying http request message: 31 | *) 32 | http { 33 | GET @"https://reqres.in/api/users?page=2&delay=3" 34 | config_transformHttpRequestMessage (fun msg -> 35 | printfn "HTTP message: %A" msg 36 | msg) 37 | } 38 | -------------------------------------------------------------------------------- /docs/Logging.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: Logging 4 | category: Documentation 5 | categoryindex: 1 6 | index: 14 7 | --- 8 | *) 9 | 10 | (** 11 | ## Turn on/off Logging 12 | 13 | How does logging work in FsHttp: 14 | 15 | - In F# interactive, console logging (via logfn) are enabled per default. 16 | - It's then possible to deactivate logging globally, using `Fsi.disableDebugLogs()` 17 | - In a non-F# interactive environment, there should be no logging at all per default. 18 | - It's possible to turn on logging via `Fsi.enableDebugLogs()` (which is also applied to non-interactive environments). 19 | 20 | *) -------------------------------------------------------------------------------- /docs/Overview.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: Overview 4 | category: Documentation 5 | categoryindex: 1 6 | index: 2 7 | --- 8 | *) 9 | 10 | (*** condition: prepare ***) 11 | #nowarn "211" 12 | #r "../src/FsHttp/bin/Release/net6.0/FsHttp.dll" 13 | open FsHttp 14 | 15 | 16 | 17 | (** 18 | ## Installing 19 | *) 20 | // Reference the 'FsHttp' package from NuGet in your script or project 21 | #r "nuget: FsHttp" 22 | 23 | open FsHttp 24 | 25 | (** 26 | ## Performing a GET request: 27 | *) 28 | http { 29 | GET "https://mysite" 30 | AcceptLanguage "en-US" 31 | } 32 | |> Request.send 33 | 34 | (** 35 | The request is sent synchronously in the example above. See (TODO: `async` / `task`) section to see how requests and responses can be processed using `async` or `task` abstractions. 36 | 37 | > **For production use, it is recommended using `async` or `task` based functions!** 38 | 39 | ## Performing a POST request with JSON object content: 40 | *) 41 | http { 42 | POST "https://mysite" 43 | 44 | body 45 | jsonSerialize 46 | {| 47 | name = "morpheus" 48 | job = "leader" 49 | |} 50 | } 51 | |> Request.send 52 | 53 | (** 54 | There are more ways of how requests definition can look: See (here)TODO for an explanation of how to multipart, form data, file upload, streaming, and more. 55 | 56 | ## Process response content as JSON: 57 | *) 58 | 59 | // Assume this returns: { "name": "Paul"; "age": 54 } 60 | let name,age = 61 | http { 62 | GET "https://mysite" 63 | AcceptLanguage "en-US" 64 | } 65 | |> Request.send 66 | |> Response.toJson 67 | |> fun json -> json?name.GetString(), json?age.GetInt32() 68 | 69 | (** 70 | 71 | Use the `?` operator to access JSON properties. The `GetString()`, `GetInt32()` and similar methods are used to convert the JSON values to the desired type. They are defined as extension methods in `System.Text.Json.JsonElement`. 72 | 73 | **FSharp.Data and Newtonsoft.Json** 74 | 75 | Per default, `System.Text.Json` is used as backend for dealing with JSON responses. If prefer `FSharp.Data` or `Newtonsoft.Json`, you can use the extension packages (see here(TODO)). 76 | 77 | ## Configuration 78 | *) 79 | 80 | // A configuration per request 81 | http { 82 | GET "https://mysite" 83 | AcceptLanguage "en-US" 84 | 85 | // This can be placed anywhere in the request definition. 86 | config_timeoutInSeconds 10.0 87 | } 88 | |> Request.send 89 | 90 | (** 91 | There are many ways of configuring a request - from simple config values like above, to changing or replacing the underlying `System.Net.Http.HttpClient` and `System.Net.Http.HttpRequestMessage` (have a look here()TODO). 92 | 93 | It is also possible to set configuration values globally: 94 | *) 95 | 96 | GlobalConfig.defaults 97 | |> Config.timeoutInSeconds 11.1 98 | |> GlobalConfig.set 99 | -------------------------------------------------------------------------------- /docs/Release_Notes.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: Release Notes / Migrations 4 | category: Documentation 5 | categoryindex: 1 6 | index: 10 7 | --- 8 | *) 9 | 10 | (** 11 | ## Migrations 12 | 13 | ### To v9 (Breaking Changes) 14 | 15 | * `http...` Builders: There is now only a single `http` builder, that is equivalent to the former `httpLazy` builder. To achieve the behaviour of the removed builders, please use: 16 | * `httpLazy` -> `http { ... }` 17 | * `http` -> `http { ... } |> Request.send` 18 | * `httpAsync` -> `http { ... } |> Request.sendAsync` 19 | * `httpLazyAsync` -> `http { ... } |> Request.toAsync` 20 | * `httpMessage` -> `http { ... } |> Request.toMessage` 21 | * see also: [https://github.com/fsprojects/FsHttp/blob/master/src/Tests/BuildersAndSignatures.fs](Tests in BuildersAndSignatures.fs) 22 | * Renamed type `LazyHttpBuilder` -> `HttpBuilder` 23 | * Renamed `Request.buildAsync` -> `Request.toAsync` 24 | * Removed `send` and `sendAsync` builder methods 25 | * Changed request and response printing (mostly used in FSI) 26 | * Printing related custom operations change in names and behaviour 27 | * `Dsl` / `DslCE` namespaces: There is no need for distinction of both namespaces. It is now sufficient to `open FsHttp` only. 28 | * The `HttpBuilder<'context>` is replaced by `IBuilder<'self>`, so that the CE methods work directly on the `HeaderContext`, `BodyContext`, and `MultipartContext` directly. This simplifies things like mixing Dsl and DslCE, pre-configuring and chaining requests. 29 | * The global configuration is now in the `FsHttp.GlobalConfig` module. The `Config` module is only for functions on request contexts. 30 | * QueryParams is `(string * obj) list` now 31 | * Use of System.Text.Json as a standard JSON library and created separate Newtonsoft and FSharp.Data JSON packages. 32 | * Dropped support for .Net Standard 2.0 33 | * Smaller breaking changes 34 | 35 | ### To > v10 36 | 37 | * See [https://www.nuget.org/packages/FsHttp#release-body-tab](Directory.Build.props) 38 | 39 | *) 40 | -------------------------------------------------------------------------------- /docs/Request_Headers.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: Request Headers 4 | category: Documentation 5 | categoryindex: 1 6 | index: 4 7 | --- 8 | *) 9 | 10 | (*** condition: prepare ***) 11 | #nowarn "211" 12 | #r "../src/FsHttp/bin/Release/net6.0/FsHttp.dll" 13 | open FsHttp 14 | 15 | 16 | (** 17 | ## Specifying Standard and Custom Headers 18 | 19 | * The standard headers can be specified using the corresponding custom operation names. 20 | * Example: `AuthorizationBearer` (a shortcur for the "Authorization" header with the "Bearer" scheme) 21 | * Example: `Accept` 22 | * Example: `UserAgent` 23 | * Custom headers can be specified using the `header` function. 24 | * Example: `header "X-GitHub-Api-Version" "2022-11-28"` 25 | 26 | Here's an example that fetches the issues of the vide-collabo/vide repository on GitHub: 27 | *) 28 | 29 | http { 30 | GET "https://api.github.com/repos/vide-collabo/vide/issues" 31 | 32 | AuthorizationBearer "**************" 33 | Accept "application/vnd.github.v3+json" 34 | UserAgent "FsHttp" 35 | header "X-GitHub-Api-Version" "2022-11-28" 36 | } 37 | |> Request.send 38 | -------------------------------------------------------------------------------- /docs/Requesting_FormData.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: POST Form Data 4 | category: Documentation 5 | categoryindex: 2 6 | index: 6 7 | --- 8 | *) 9 | 10 | (*** condition: prepare ***) 11 | #nowarn "211" 12 | #r "../src/FsHttp/bin/Release/net6.0/FsHttp.dll" 13 | open FsHttp 14 | 15 | 16 | 17 | (** 18 | ## Sending URL-Encoded Form 19 | *) 20 | http { 21 | POST "https://mysite" 22 | body 23 | formUrlEncoded [ 24 | "key_1", "Data 1" 25 | "key_2", "Data 2" 26 | ] 27 | } 28 | |> Request.send 29 | -------------------------------------------------------------------------------- /docs/Requesting_Multipart_Files.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: Multipart and File Upload 4 | category: Documentation 5 | categoryindex: 2 6 | index: 5 7 | --- 8 | *) 9 | 10 | (*** condition: prepare ***) 11 | #nowarn "211" 12 | #r "../src/FsHttp/bin/Release/net6.0/FsHttp.dll" 13 | open FsHttp 14 | 15 | 16 | 17 | (** 18 | ## Sending Multipart Form-Data 19 | 20 | **Performing a POST multipart request / uploading a file:** 21 | *) 22 | http { 23 | POST "https://mysite" 24 | 25 | // use "multipart" keyword (instead of 'body') to start specifying parts 26 | multipart 27 | textPart "the-actual-value_1" "the-part-name_1" 28 | textPart "the-actual-value_2" "the-part-name_2" 29 | filePart "super.txt" "F# rocks!" 30 | } 31 | |> Request.send 32 | 33 | (** 34 | ## Further Readings 35 | 36 | > Have a look at the [https://github.com/fsprojects/FsHttp/blob/master/src/Tests/Multipart.fs](multipart tests) for more examples using multipart. 37 | *) -------------------------------------------------------------------------------- /docs/Response_Handling.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: Response Handling 4 | category: Documentation 5 | categoryindex: 1 6 | index: 8 7 | --- 8 | *) 9 | 10 | (*** condition: prepare ***) 11 | #nowarn "211" 12 | #r "../src/FsHttp/bin/Release/net6.0/FsHttp.dll" 13 | open FsHttp 14 | 15 | 16 | (** 17 | ## Response Content Transformations 18 | 19 | There are several ways transforming the content of the returned response to 20 | something like text or JSON: 21 | 22 | See also: [Response](reference/FsHttp-response.html) 23 | *) 24 | http { 25 | POST "https://reqres.in/api/users" 26 | CacheControl "no-cache" 27 | body 28 | json """ 29 | { 30 | "name": "morpheus", 31 | "job": "leader" 32 | } 33 | """ 34 | } 35 | |> Request.send 36 | |> Response.toJson 37 | 38 | 39 | 40 | (** 41 | ## JSON dynamic processing: 42 | *) 43 | 44 | http { 45 | GET @"https://reqres.in/api/users?page=2&delay=3" 46 | } 47 | |> Request.send 48 | |> Response.toJson 49 | |> fun json -> json?page.GetInt32() 50 | 51 | 52 | 53 | (** 54 | ## JsonSerializerOptions / Using Tarmil-FSharp.SystemTextJson 55 | 56 | FSharp.SystemTextJson enables JSON (de)serialization of F# types like tuples, DUs and others. 57 | To do so, use the `JsonSerializeWith` or one of the `Response.toJsonWith` functions and pass 58 | `JsonSerializerOptions`. Instead, it's also possible to globally configure the `JsonSerializerOptions` 59 | that will be used as default for any request where JSON (de)serialization is involved: 60 | *) 61 | 62 | #r "nuget: FSharp.SystemTextJson" 63 | 64 | // --------------------------------- 65 | // Prepare global JSON configuration 66 | // --------------------------------- 67 | 68 | open System.Text.Json 69 | open System.Text.Json.Serialization 70 | 71 | FsHttp.GlobalConfig.Json.defaultJsonSerializerOptions <- 72 | let options = JsonSerializerOptions() 73 | options.Converters.Add(JsonFSharpConverter()) 74 | options 75 | 76 | // ----------------- 77 | // ... make requests 78 | // ----------------- 79 | 80 | type Person = { name: string; age: int; address: string option } 81 | let john = { name ="John"; age = 23; address = Some "whereever" } 82 | 83 | http { 84 | POST "loopback body" 85 | body 86 | jsonSerialize john 87 | } 88 | |> Request.send 89 | |> Response.deserializeJson 90 | |> fun p -> p.address = john.address // true 91 | 92 | (** 93 | ## Download a file 94 | 95 | You can easily save the response content as file: 96 | *) 97 | 98 | // Downloads the nupkg file as zip 99 | get "https://www.nuget.org/api/v2/package/G-Research.FSharp.Analyzers/0.1.5" 100 | |> Request.send 101 | |> Response.saveFile @"C:\Downloads\analyzers.zip" 102 | -------------------------------------------------------------------------------- /docs/Sending_Requests.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: Sending Requests 4 | category: Documentation 5 | categoryindex: 2 6 | index: 7 7 | --- 8 | *) 9 | 10 | (*** condition: prepare ***) 11 | #nowarn "211" 12 | #r "../src/FsHttp/bin/Release/net6.0/FsHttp.dll" 13 | open FsHttp 14 | open System.Threading 15 | 16 | 17 | 18 | (** 19 | ## JSON String 20 | *) 21 | http { 22 | POST "https://mysite" 23 | 24 | body 25 | json """ 26 | { 27 | "name" = "morpheus", 28 | "job" = "leader", 29 | "age": %d 30 | } 31 | """ 32 | } 33 | |> Request.send 34 | 35 | 36 | (** 37 | **Parametrized JSON:** 38 | 39 | When the JSON content needs to be parametrized, `sprintf` function is a useful tool. Compared to interpolated strings, the curly braces - which are a key character in JSON - don't have to be escaped: 40 | *) 41 | let sendRequestWithSprintf age = 42 | http { 43 | POST "https://mysite" 44 | 45 | body 46 | json (sprintf """ 47 | { 48 | "name" = "morpheus", 49 | "job" = "leader", 50 | "age": %d 51 | } 52 | """ age) 53 | } 54 | |> Request.send 55 | 56 | 57 | (** 58 | **Using an interpolated string:** 59 | *) 60 | let sendRequestWithInterpolatedString age = 61 | http { 62 | POST "https://mysite" 63 | 64 | body 65 | json $""" 66 | {{ 67 | "name" = "morpheus", 68 | "job" = "leader", 69 | "age": {age} 70 | }} 71 | """ 72 | } 73 | |> Request.send 74 | 75 | (** 76 | ## Sending records or objects as JSON 77 | *) 78 | http { 79 | POST "https://mysite" 80 | 81 | body 82 | jsonSerialize 83 | {| 84 | name = "morpheus" 85 | job = "leader" 86 | |} 87 | } 88 | |> Request.send 89 | 90 | (** 91 | > It is also possible to pass serializer settings using the `jsonSerializeWith` operation. 92 | *) 93 | 94 | 95 | (** 96 | ## Plain text 97 | *) 98 | http { 99 | POST "https://mysite" 100 | body 101 | // Sets Content-Type: plain/text header 102 | text """ 103 | The last train is nearly due 104 | The underground is closing soon 105 | And in the dark deserted station 106 | Restless in anticipation 107 | A man waits in the shadows 108 | """ 109 | } 110 | |> Request.send 111 | 112 | (** 113 | ## Request Cancellation 114 | 115 | It is possible to bind a cancellation token to a request definition, 116 | that will be used for the underlying HTTP request: 117 | *) 118 | 119 | use cts = new CancellationTokenSource() 120 | 121 | // ... 122 | 123 | http { 124 | GET "https://mysite" 125 | config_cancellationToken cts.Token 126 | } 127 | |> Request.send 128 | 129 | 130 | (** 131 | See also: https://github.com/fsprojects/FsHttp/issues/105 132 | 133 | Instead of binding a cancellation token directly to a request definition (like in the example above), 134 | it is also possible to pass it on execution-timer, like so: 135 | *) 136 | 137 | http { 138 | GET "https://mysite" 139 | } 140 | |> Config.cancellationToken cts.Token 141 | |> Request.send 142 | 143 | -------------------------------------------------------------------------------- /docs/URLs_and_Query_Params.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | --- 3 | title: URLs and Query Params 4 | category: Documentation 5 | categoryindex: 1 6 | index: 3 7 | --- 8 | *) 9 | 10 | (*** condition: prepare ***) 11 | #nowarn "211" 12 | #r "../src/FsHttp/bin/Release/net6.0/FsHttp.dll" 13 | open FsHttp 14 | 15 | 16 | 17 | (** 18 | ## URLs (Line Breaks and Comments): 19 | 20 | You can split URL query parameters or comment lines out by using F# line-comment syntax. Line breaks and trailing or leading spaces will be removed: 21 | *) 22 | 23 | http { 24 | GET "https://mysite 25 | ?page=2 26 | //&skip=5 27 | &name=Hans" 28 | } 29 | |> Request.send 30 | 31 | 32 | (** 33 | ## Query Parameters 34 | 35 | It's also possible to specify query params in a list: 36 | *) 37 | 38 | http { 39 | GET "https://mysite" 40 | query [ 41 | "page", "2" 42 | "name", "Hans" 43 | ] 44 | } 45 | |> Request.send 46 | 47 | (** 48 | **Please note:** Using F# version 5 or lower, an upcast of the parameter values is needed! 49 | *) -------------------------------------------------------------------------------- /docs/content/fsdocs-theme.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --FsHttp-50: #f0fdf2; 3 | --FsHttp-100: #dbfde3; 4 | --FsHttp-200: #baf8c8; 5 | --FsHttp-300: #64ee85; 6 | --FsHttp-400: #47e16c; 7 | --FsHttp-500: #1ec948; 8 | --FsHttp-600: #13a637; 9 | --FsHttp-700: #12832e; 10 | --FsHttp-800: #14672a; 11 | --FsHttp-900: #135425; 12 | --FsHttp-950: #042f11; 13 | --logo-background-color: #224454; 14 | --pearl: #FFFDFA; 15 | 16 | --primary: var(--FsHttp-300); 17 | --header-background: var(--logo-background-color); 18 | --header-link-color: var(--pearl); 19 | --fsdocs-theme-toggle-light-color: var(--pearl); 20 | --menu-color: var(--pearl); 21 | --blockquote-bacground-color: var(--primary); 22 | --blockquote-color: var(--pearl); 23 | --menu-item-hover-background: var(--FsHttp-200); 24 | --menu-item-hover-color: var(--logo-background-color); 25 | --on-this-page-color: var(--pearl); 26 | --heading-color: var(--FsHttp-950); 27 | --page-menu-background-hover-border-color: var(--FsHttp-300); 28 | --nav-item-active-border-color: var(--FsHttp-300); 29 | --dialog-icon-color: var(--FsHttp-300); 30 | --dialog-link-color: var(--FsHttp-300); 31 | } 32 | 33 | [data-theme=dark] { 34 | --link-color: var(--FsHttp-200); 35 | --heading-color: var(--pearl); 36 | } -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FsHttp/cd7e362bba6dfd1e1cdb3d357a11dc8468ea8d7b/docs/img/logo.png -------------------------------------------------------------------------------- /docs/img/logo_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FsHttp/cd7e362bba6dfd1e1cdb3d357a11dc8468ea8d7b/docs/img/logo_big.png -------------------------------------------------------------------------------- /docs/img/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FsHttp/cd7e362bba6dfd1e1cdb3d357a11dc8468ea8d7b/docs/img/logo_small.png -------------------------------------------------------------------------------- /docu-TODOs.md: -------------------------------------------------------------------------------- 1 | - Integration packages - Newtonsoft, FsharpData 2 | 3 | 4 | Making Requests 5 | - Sending 6 | - Transforming 7 | 8 | Configuration 9 | - Configuration 10 | 11 | JSON 12 | - System.Text.JSON 13 | - ? Operator: Working with Json 14 | - FsHttp.FSharpData package 15 | 16 | CSharp 17 | 18 | FSI: 19 | - % Operator 20 | 21 | Async 22 | --- 23 | Async.await / Async.map 24 | 25 | File Upload 26 | 27 | * Referenzen zu Tests in jeder Datei, wo es sich anbietet 28 | 29 | 30 | 31 | (** 32 | An alternative way: HTTP method-first functions 33 | *) 34 | get "https://mysite" { 35 | AcceptLanguage "en-US" 36 | } 37 | |> Request.sendAsync 38 | 39 | 40 | 41 | 42 | (** 43 | Working in F# Interactive or notebooks, a short form for sending requests can be used: The `%` operator. 44 | 45 | > Note: Since the `%` operator send a synchronous request (blocking the caller thread), 46 | > it is only recommended for using in an interactive environment. 47 | *) 48 | open FsHttp.Operators 49 | 50 | % http { 51 | GET "https://mysite" 52 | AcceptLanguage "en-US" 53 | } 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | expect / assert 63 | 64 | 65 | Async: 66 | diff betweet send and to 67 | task "...TAsync functions" 68 | pipeline style (await / map) und CE-style 69 | 70 | 71 | 72 | 73 | (** 74 | Process response content as JSON: 75 | 76 | Hint: 77 | * HOT: 'sendAsync' sends the request immediately. 78 | * COLD: 'toAsync' builds an async request that needs to be started. 79 | 80 | *) 81 | 82 | let asyncResponse = 83 | // Assume this returns: { "name": "Paul"; "age": 54 } 84 | http { 85 | GET "https://mysite" 86 | AcceptLanguage "en-US" 87 | } 88 | |> Request.sendAsync 89 | |> Async.await Response.toJsonAsync 90 | |> Async.map (fun json -> json?name.GetString(), json?age.GetInt32()) 91 | 92 | 93 | DSL Syntax (Pipeline / mixing this) 94 | 95 | 96 | 97 | 98 | * Per-defining buidlers 99 | 100 | 101 | // A configuration per request 102 | http { 103 | config_timeoutInSeconds 10.0 104 | } 105 | 106 | -------------------------------------------------------------------------------- /docu-watch.sh: -------------------------------------------------------------------------------- 1 | ./docu.sh watch -------------------------------------------------------------------------------- /docu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docuIndexYamlHeader="--- 4 | IMPORTANT: This file is generated by \`./docu.sh\`. Please don't edit it manually! 5 | 6 | title: FsHttp Overview 7 | index: 1 8 | --- 9 | 10 | " 11 | readmeContent=$(cat ./README.md) 12 | echo "$docuIndexYamlHeader$readmeContent" > ./docs/index.md 13 | 14 | if [ -d ./.fsdocs ]; then 15 | rm -rf ./.fsdocs 16 | fi 17 | 18 | dotnet tool restore 19 | dotnet build ./src/FsHttp/FsHttp.fsproj -c Release -f net8.0 20 | 21 | # what a hack... 22 | if [ -z "$1" ]; then 23 | mode="build" 24 | else 25 | mode="watch" 26 | fi 27 | 28 | dotnet fsdocs \ 29 | $mode \ 30 | --clean \ 31 | --sourcefolder ./src \ 32 | --properties Configuration=Release TargetFramework=net8.0 \ 33 | --sourcerepo https://github.com/fsprojects/FsHttp/blob/master/src \ 34 | --parameters root /FsHttp/ \ 35 | --output ./.docsOutput \ 36 | --ignoreprojects Test.CSharp.csproj \ 37 | --strict 38 | -------------------------------------------------------------------------------- /fiddle/DUShadowingInCEs.fsx: -------------------------------------------------------------------------------- 1 | #r "../src/FsHttp/bin/debug/net7.0/FsHttp.dll" 2 | 3 | open System 4 | open FsHttp 5 | 6 | type ArgA = 7 | | FileName of string 8 | | Path of string 9 | 10 | type ArgB = 11 | | FileName of string 12 | | AnotherPath of string 13 | 14 | type IRequestContext<'self> with 15 | [] 16 | member _.A(context: IRequestContext, name, [] argA: ArgA array) : MultipartContext = 17 | failwith "TODO: implement me" 18 | 19 | [] 20 | member _.B(context: IRequestContext, name, [] argA: ArgB array) : MultipartContext = 21 | failwith "TODO: implement me" 22 | 23 | http { 24 | POST("") 25 | multipart 26 | 27 | a "Resources/uploadFile.txt" (FileName "xsycy") (Path "xsycy") 28 | } 29 | -------------------------------------------------------------------------------- /fiddle/assertDslCeMethods.fsx: -------------------------------------------------------------------------------- 1 | open System 2 | open System.IO 3 | open System.Reflection 4 | open FSharp.Reflection 5 | 6 | let path = Path.Combine(__SOURCE_DIRECTORY__, "../FsHttp/bin/Debug/net6.0/FsHttp.dll") 7 | let asm = Assembly.LoadFile(path) 8 | 9 | //let dslHeaderType = asm.GetExportedTypes() |> Array.map (fun t -> t.FullName) 10 | 11 | let findType typeName = asm.GetExportedTypes() |> Array.find (fun t -> t.FullName = typeName) 12 | 13 | let getMethodsOfType typeName = 14 | let t = findType typeName 15 | let methods = 16 | t.GetMethods() 17 | |> Array.map (fun m -> m.Name) 18 | |> Array.except [| "GetType"; "ToString"; "Equals"; "GetHashCode" |] 19 | do 20 | let amb = 21 | methods 22 | |> Array.groupBy id 23 | |> Array.filter (fun (_,values) -> values.Length > 1) 24 | |> Array.map fst 25 | if amb |> Array.length > 0 then 26 | failwith $"""Ambiguous methods: {String.concat ";" amb}""" 27 | methods 28 | 29 | let getDslFunctions typeName = 30 | getMethodsOfType typeName 31 | 32 | let getDslCEMethods typeName = 33 | getMethodsOfType typeName 34 | 35 | 36 | 37 | let dslHeaderMethods = getDslFunctions "FsHttp.Dsl+Header" 38 | let dslCeHeaderMethods = getDslCEMethods "FsHttp.DslCE+Header" 39 | -------------------------------------------------------------------------------- /fiddle/builderPlayground.fsx: -------------------------------------------------------------------------------- 1 | 2 | module A = 3 | type StartingContext() = class end 4 | 5 | type HeaderContext = { 6 | url: string 7 | method: string 8 | headers: (string * string) list } 9 | 10 | type StartingContext with 11 | member this.Yield(_) = this 12 | 13 | [] 14 | member this.Get(_: StartingContext, url) = { url = url; method = "GET"; headers = [] } 15 | 16 | 17 | type HeaderContext with 18 | member this.Yield(_) = this 19 | 20 | [] 21 | member this.Accept(context, contentType) = this 22 | 23 | 24 | let http = StartingContext() 25 | 26 | http { 27 | GET "xxx" 28 | Accept "text/json" 29 | } 30 | 31 | module B = 32 | type HeaderContext = { 33 | url: string 34 | method: string 35 | headers: (string * string) list } 36 | 37 | type HeaderContext with 38 | member this.Yield(_) = this 39 | 40 | [] 41 | member this.Get(_: HeaderContext, url) = { url = url; method = "GET"; headers = [] } 42 | 43 | [] 44 | member this.Accept(context, contentType) = this 45 | 46 | 47 | let http = { url = ""; method = "GET"; headers = [] } 48 | 49 | http { 50 | GET "xxx" 51 | Accept "text/json" 52 | } 53 | 54 | 55 | 56 | module C = 57 | 58 | [] 59 | module BuilderExtensions = 60 | type IBuilder<'implementor> = interface end 61 | 62 | type StartingContext() = 63 | interface IBuilder 64 | 65 | type HeaderContext = { 66 | url: string 67 | method: string 68 | headers: (string * string) list } with 69 | interface IBuilder 70 | 71 | type IBuilder<'implementor> with 72 | member this.Yield(_) = this 73 | 74 | type StartingContext with 75 | [] 76 | member this.Get(context: IBuilder, url) = 77 | { url = url; method = "GET"; headers = [] } 78 | 79 | type IBuilder<'implementor> with 80 | [] 81 | member this.Accept(context: IBuilder, contentType) = 82 | context 83 | 84 | let http = StartingContext() 85 | 86 | http { 87 | GET "xxx" 88 | Accept "text/json" 89 | } 90 | -------------------------------------------------------------------------------- /fiddle/co-maintainer.fsx: -------------------------------------------------------------------------------- 1 | 2 | #r "../src/FsHttp/bin/debug/net7.0/FsHttp.dll" 3 | #r "nuget: FsHttp" 4 | 5 | open System 6 | open System.Net.Http 7 | open FsHttp 8 | open FsHttp.Operators 9 | 10 | do Fsi.disableDebugLogs() 11 | 12 | http { 13 | GET "https://www.wikipedia.de" 14 | Authorization c 15 | } 16 | |> Request.sendAsync 17 | 18 | 19 | 20 | get "https://www.wikipedia.de" 21 | |> Header.authorization "https://www.wikipedia.de" 22 | 23 | 24 | get "https://www.wikipedia.de" { 25 | Authorization c 26 | } 27 | |> Header.acceptCharset "whatever" 28 | 29 | 30 | // ----------------- 31 | // F# Interactive 32 | // ----------------- 33 | 34 | open FsHttp.Operators 35 | 36 | % get "https://www.wikipedia.de" { 37 | AcceptCharset "" 38 | } 39 | 40 | get "https://www.wikipedia.de" { 41 | AcceptCharset "" 42 | } 43 | |> Request.send 44 | 45 | 46 | 47 | // ----------------- 48 | // Composability 49 | // ----------------- 50 | 51 | let httpLongRunning = 52 | http { 53 | config_timeoutInSeconds 100.0 54 | } 55 | 56 | let waitLongForGoogle = 57 | httpLongRunning { 58 | GET "https://www.google.de" 59 | } 60 | 61 | waitLongForGoogle |> Request.send 62 | 63 | // ----------------- 64 | // Use Cases 65 | // ----------------- 66 | 67 | let makeEnv serverName (method: string -> HeaderContext) urlSuffix = 68 | method (serverName urlSuffix) 69 | 70 | let env1 = makeEnv "http://localhost/myService" 71 | let env2 = makeEnv "https://www.google.de" 72 | 73 | 74 | % env2 get "xxx" { 75 | AcceptCharset "" 76 | } 77 | 78 | 79 | 80 | http { 81 | // IStartingContext ... 82 | 83 | GET "http://www.google.de" 84 | // IHeaderContext ... 85 | 86 | AcceptCharset "" 87 | 88 | body 89 | // IBodyContext ... 90 | 91 | json """ [1] """ 92 | // IFinalContext (has no operations defined at all) 93 | } 94 | 95 | 96 | http { 97 | GET "http://...." 98 | } 99 | |> Request.toHttpRequestMessage 100 | 101 | 102 | |> Request.send 103 | |> Response.toJsonDocument 104 | -------------------------------------------------------------------------------- /fiddle/debugBuildFiddle.fsx: -------------------------------------------------------------------------------- 1 | 2 | #r "../src/FsHttp/bin/Debug/net6.0/FsHttp.dll" 3 | 4 | open FsHttp 5 | 6 | let httpWithAuth = 7 | http { 8 | AuthorizationBearer "sdfsdffsdf" 9 | } 10 | 11 | httpWithAuth { 12 | GET "https://jsonplaceholder.typicode.com/todos/1" 13 | } 14 | |> Request.send 15 | -------------------------------------------------------------------------------- /fiddle/discuss-119-Testing.fsx: -------------------------------------------------------------------------------- 1 | 2 | #r "nuget: FsHttp" 3 | #r "nuget: Suave" 4 | 5 | open System 6 | open System.Threading 7 | 8 | open FsHttp 9 | 10 | open Suave 11 | open Suave.Operators 12 | open Suave.Successful 13 | 14 | module Intercept = 15 | 16 | let basePort = 8080 17 | let baseAdapter = "127.0.0.1" 18 | 19 | let makeUrl (s: string) = $"http://{baseAdapter}:{basePort}{s}" 20 | 21 | let serve (app: WebPart) = 22 | let cts = new CancellationTokenSource() 23 | let conf = 24 | { defaultConfig with 25 | cancellationToken = cts.Token 26 | bindings = [ HttpBinding.createSimple HTTP baseAdapter basePort ] 27 | } 28 | let listening, server = startWebServerAsync conf app 29 | do Async.Start(server, cts.Token) 30 | do 31 | listening 32 | |> Async.RunSynchronously 33 | |> Array.choose id 34 | |> Array.map (fun x -> x.binding |> string) 35 | |> String.concat "; " 36 | |> printfn "Server ready and listening on: %s" 37 | 38 | let dispose () = 39 | cts.Cancel() 40 | cts.Dispose() 41 | { new System.IDisposable with 42 | member _.Dispose() = dispose () } 43 | 44 | type Wire = { method: WebPart; url: Uri; rewrite: string; resp: WebPart } 45 | 46 | let rewire (wires: Wire list) = 47 | do 48 | GlobalConfig.defaults 49 | |> Config.transformHttpRequestMessage (fun msg -> 50 | let requestedUriWithoutQuery = msg.RequestUri.GetLeftPart(UriPartial.Path) 51 | let wire = 52 | wires 53 | |> List.tryFind (fun wire -> Uri(requestedUriWithoutQuery) = wire.url) 54 | |> Option.map (fun x -> makeUrl x.rewrite) 55 | match wire with 56 | | Some wire -> 57 | msg.RequestUri <- Uri(wire) 58 | | None -> 59 | failwith "NO FOUND" 60 | () 61 | msg 62 | ) 63 | |> GlobalConfig.set 64 | let stopServer = 65 | wires 66 | |> List.map (fun x -> x.method >=> Filters.path x.rewrite >=> x.resp) 67 | |> choose 68 | |> serve 69 | let dispose () = 70 | stopServer.Dispose() 71 | GlobalConfig.defaults |> Config.transformHttpRequestMessage id |> GlobalConfig.set 72 | { new System.IDisposable with 73 | member _.Dispose() = dispose () } 74 | 75 | open Intercept 76 | 77 | let myTestCase () = 78 | 79 | // Intercept a GET on wikipedia and return 200 with content "You know it" 80 | use _ = 81 | [ 82 | { 83 | method = Filters.GET 84 | url = Uri("https://www.wikipedia.de") 85 | rewrite = "/" 86 | resp = OK "You know it" 87 | } 88 | ] 89 | |> rewire 90 | 91 | let responseText = 92 | http { 93 | GET "https://www.wikipedia.de" 94 | } 95 | |> Request.send 96 | |> Response.toText 97 | 98 | if responseText <> "You know it" then 99 | failwith "Test failed." 100 | -------------------------------------------------------------------------------- /fiddle/dotnet_fsi_net5/fshttp_with_net5_error.fsx: -------------------------------------------------------------------------------- 1 | // dotnet fsi with SDK 5 and FsHttp version 8.0.0 will hang when making the request below! 2 | #r "nuget: FsHttp, 8.0.0" 3 | 4 | open FsHttp 5 | open FsHttp.DslCE 6 | open FsHttp.Operators 7 | 8 | % get "https://www.google.de" 9 | |> Response.toText 10 | |> String.length 11 | -------------------------------------------------------------------------------- /fiddle/dotnet_fsi_net5/fshttp_with_net5_success.fsx: -------------------------------------------------------------------------------- 1 | // dotnet fsi with SDK 5 and FsHttp version 8.0.1 will work 2 | #r "nuget: FsHttp, 8.0.1" 3 | 4 | open FsHttp 5 | open FsHttp.DslCE 6 | open FsHttp.Operators 7 | 8 | % get "https://www.google.de" 9 | |> Response.toText 10 | |> String.length 11 | -------------------------------------------------------------------------------- /fiddle/dotnet_fsi_net5/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "5.0.0", 4 | "allowPrerelease": false, 5 | "rollForward": "latestMinor" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fiddle/dotnet_fsi_net6/fshttp_with_net6_success.fsx: -------------------------------------------------------------------------------- 1 | // dotnet fsi with SDK 6 and FsHttp version 8.0.1 will work 2 | #r "nuget: FsHttp, 8.0.1" 3 | 4 | open FsHttp 5 | open FsHttp.DslCE 6 | open FsHttp.Operators 7 | 8 | % get "https://www.google.de" 9 | |> Response.toText 10 | |> String.length 11 | -------------------------------------------------------------------------------- /fiddle/dotnet_fsi_net6/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "6.0.0", 4 | "allowPrerelease": false, 5 | "rollForward": "latestMinor" 6 | } 7 | } -------------------------------------------------------------------------------- /fiddle/dotnet_fsi_net7/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "7.0.0", 4 | "allowPrerelease": false, 5 | "rollForward": "latestMinor" 6 | } 7 | } -------------------------------------------------------------------------------- /fiddle/dotnet_fsi_net7/issue-109-114-fail.fsx: -------------------------------------------------------------------------------- 1 | (* 2 | shell> dotnet fsi .\issue-109-114-fail.fsx 3 | 4 | System.FormatException: The format of value '' is invalid. 5 | at System.Net.Http.Headers.HttpHeaderParser.ParseValue(String value, Object storeValue, Int32& index) 6 | at System.Net.Http.Headers.HttpHeaders.ParseAndAddValue(HeaderDescriptor descriptor, HeaderStoreItemInfo info, String value) 7 | at System.Net.Http.Headers.HttpHeaders.Add(HeaderDescriptor descriptor, String value) 8 | at FsHttp.Request.toAsync@133-4.Invoke(CancellationToken ctok) 9 | at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, b result1, FSharpFunc`2 userCode) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 528 10 | at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 112 11 | --- End of stack trace from previous location --- 12 | at Microsoft.FSharp.Control.AsyncResult`1.Commit() in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 454 13 | at Microsoft.FSharp.Control.AsyncPrimitives.QueueAsyncAndWaitForResultSynchronously[a](CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 1140 14 | at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T](FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 1511 15 | at .$FSI_0002.main@() in C:\Users\ronal\source\repos\github\FsHttp\fiddle\dotnet_fsi_net7\issue-109-114.fsx:line 10 16 | at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) 17 | at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr) 18 | *) 19 | 20 | #r "nuget: FsHttp, 9.0.0" 21 | 22 | open FsHttp 23 | 24 | let req = http { 25 | GET "http://google.com" 26 | } 27 | 28 | let res = Request.send req 29 | 30 | printfn $"res = %A{res}" 31 | -------------------------------------------------------------------------------- /fiddle/dotnet_fsi_net7/issue-109-114-success.fsx: -------------------------------------------------------------------------------- 1 | (* 2 | shell> dotnet fsi .\issue-109-114-success.fsx 3 | *) 4 | 5 | #r "nuget: FsHttp, 10.0.0-preview2" 6 | 7 | open FsHttp 8 | 9 | let req = http { 10 | GET "http://google.com" 11 | } 12 | 13 | let res = Request.send req 14 | 15 | printfn $"res = %A{res}" 16 | -------------------------------------------------------------------------------- /fiddle/giraffe-issue.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.0) 27 | 28 | let makeRequest (Url url: Url) (queryParams: list) = 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) = 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 url = 52 | printRunTitle title 53 | 54 | let stopWatch = Diagnostics.Stopwatch.StartNew() 55 | for queryParams in qps do 56 | makeRequest url queryParams |> waitForOneSecond 57 | 58 | stopWatch.Stop() 59 | printTimeTaken stopWatch.Elapsed 60 | 61 | 62 | let runFiveRequests = run [ for _ in 1..5 do [] ] 63 | 64 | let testPublicCachedNoVaryByQueryKeys () = 65 | let allQueryParams = 66 | [ 67 | queryParams1 68 | queryParams1 69 | queryParams2 70 | queryParams2 71 | ] 72 | let title = Title "Testing the /cached/vary/not endpoint" 73 | let url = urls.publicCachedNoVaryByQueryKeys 74 | run allQueryParams title url 75 | 76 | let testCachedVaryByQueryKeys () = 77 | let title = Title "Testing the /cached/vary/yes endpoint" 78 | let url = urls.cachedVaryByQueryKeys 79 | let allQueryParams = 80 | [ 81 | queryParams1 82 | queryParams1 83 | queryParams2 84 | queryParams2 85 | ] 86 | run allQueryParams title url 87 | 88 | let main () = 89 | runFiveRequests (Title "Testing the /cached/not endpoint") urls.notCached 90 | runFiveRequests (Title "Testing the /cached/public endpoint") urls.publicCached 91 | runFiveRequests (Title "Testing the /cached/private endpoint") urls.privateCached 92 | testPublicCachedNoVaryByQueryKeys () 93 | testCachedVaryByQueryKeys () 94 | 95 | main () 96 | -------------------------------------------------------------------------------- /fiddle/issue-101.fsx: -------------------------------------------------------------------------------- 1 | #r "nuget: FsHttp" 2 | open FsHttp 3 | 4 | let getString url rjson = async { 5 | let! response = 6 | // See also: https://schlenkr.github.io/FsHttp/Migrations.html 7 | // 'httpAsync' is replaced by 'http { ... } |> Request.sendAsync' 8 | http { 9 | POST url 10 | Origin "Web3.fs" 11 | 12 | // a) 'ContentType' is an operation only valid 13 | // on the reqiest 'body' definition. 14 | // b) Setting ContentType to 'application/json' is 15 | // redundant when "json" operation is used. 16 | body 17 | ContentType "application/json" 18 | json rjson 19 | 20 | config_timeoutInSeconds 18.5 21 | } 22 | |> Request.sendAsync 23 | let! responseContent = response |> Response.toTextAsync 24 | return responseContent 25 | } 26 | 27 | // Instead of using 'asyn { ... }' CE, it sometimes is sufficient 28 | // piping async 29 | let getStringAlternative url rjson = 30 | http { 31 | POST url 32 | Origin "Web3.fs" 33 | 34 | body 35 | json rjson 36 | ContentType "application/json" 37 | 38 | config_timeoutInSeconds 18.5 39 | } 40 | |> Request.sendAsync 41 | // use 'Async.await' that works like 'bind': 42 | |> Async.await Response.toTextAsync 43 | // use 'Async.map' to transform results in an async context: 44 | |> Async.map (fun text -> text.ToUpperInvariant()) 45 | -------------------------------------------------------------------------------- /fiddle/issue-103.fsx: -------------------------------------------------------------------------------- 1 | 2 | #r "../FsHttp/bin/debug/net6.0/FsHttp.dll" 3 | 4 | FsHttp.FsiInit.doInit() 5 | 6 | 7 | open System 8 | open System.Reflection 9 | 10 | AppDomain.CurrentDomain.GetAssemblies() |> Seq.map (fun asm -> asm.FullName) |> Seq.toList 11 | 12 | -------------------------------------------------------------------------------- /fiddle/issue-106-AllowFileNameMetadata.fsx: -------------------------------------------------------------------------------- 1 | #r "../FsHttp/bin/Debug/net6.0/FsHttp.dll" 2 | 3 | open FsHttp 4 | 5 | let weather = [||] 6 | 7 | http { 8 | POST $"https://api.telegram.org/bot_apiTelegramKey/sendPhoto" 9 | 10 | multipart 11 | part (ContentData.BinaryContent weather) (Some "image/jpeg") "photo" 12 | } 13 | |> Request.send 14 | -------------------------------------------------------------------------------- /fiddle/issue-109-HeaderFormatException.fsx: -------------------------------------------------------------------------------- 1 | #r "nuget: FsHttp" 2 | 3 | open FsHttp 4 | 5 | http { 6 | POST "https://api.jspm.io/generate" 7 | body 8 | json """ 9 | { 10 | "install": [ "lodash" ], 11 | "env": [ "browser", "module" ], 12 | "graph":true, 13 | "provider": "jspm" 14 | } 15 | """ 16 | } 17 | |> Request.send -------------------------------------------------------------------------------- /fiddle/issue-113.fsx: -------------------------------------------------------------------------------- 1 | 2 | #r "../src/FsHttp/bin/debug/net7.0/FsHttp.dll" 3 | 4 | open System 5 | open System.Net.Http 6 | open FsHttp 7 | 8 | 9 | let doPrint (client : HttpClient) = 10 | printfn "TIMEOUT: %A" client.Timeout 11 | client 12 | 13 | 14 | module A = 15 | 16 | GlobalConfig.defaults() 17 | |> Config.timeoutInSeconds 0.1 18 | |> GlobalConfig.set 19 | 20 | http { 21 | GET "https://www.google.de" 22 | config_transformHttpClient doPrint 23 | } 24 | |> Request.send 25 | 26 | 27 | module B = 28 | 29 | GlobalConfig.defaults() 30 | |> Config.timeoutInSeconds 0.1 31 | |> GlobalConfig.set 32 | 33 | http { 34 | GET "https://www.google.de" 35 | //config_transformHttpClient doPrint 36 | } 37 | |> Request.send 38 | 39 | -------------------------------------------------------------------------------- /fiddle/issue-121.fsx: -------------------------------------------------------------------------------- 1 | 2 | #r "../src/FsHttp/bin/debug/net7.0/FsHttp.dll" 3 | 4 | open System 5 | open System.Net.Http 6 | open FsHttp 7 | 8 | do Fsi.disableDebugLogs() 9 | 10 | http { 11 | GET "https://www.wikipedia.de" 12 | } 13 | |> Request.sendAsync 14 | -------------------------------------------------------------------------------- /fiddle/issue-126.fsx: -------------------------------------------------------------------------------- 1 | 2 | #r "../src/FsHttp/bin/debug/net7.0/FsHttp.dll" 3 | 4 | open System.IO 5 | open System.Net.Http 6 | open FsHttp 7 | 8 | let request = task { 9 | let! theContent = File.ReadAllTextAsync("c:/temp/content.txt") 10 | return http { 11 | GET "https://www.wikipedia.de" 12 | config_transformHttpRequestMessage (fun msg -> 13 | msg.Content <- new TextContent(theContent) 14 | msg 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fiddle/issue-129.fsx: -------------------------------------------------------------------------------- 1 | 2 | #r "../src/FsHttp/bin/debug/net7.0/FsHttp.dll" 3 | 4 | open System.IO 5 | open System.Net.Http 6 | open System.Net.Http.Headers 7 | open FsHttp 8 | 9 | let superBodyContentType = { ContentType.value = "superBody"; charset = None } 10 | 11 | type IRequestContext<'self> with 12 | [] 13 | member this.SuperBody(context: IRequestContext, csvContent: string) = 14 | FsHttp.Dsl.Body.content superBodyContentType (TextContent csvContent) context.Self 15 | 16 | MediaTypeHeaderValue.Parse("text/xxx; charset=utf-8") 17 | 18 | 19 | http { 20 | POST "http://www.google.de" 21 | body 22 | superBody "Hello" 23 | print_useObjectFormatting 24 | } 25 | 26 | http { 27 | POST "http://www.google.de" 28 | multipart 29 | 30 | textPart "Resources/uploadFile.txt" "name1" 31 | ContentType "text/json" 32 | 33 | textPart "Resources/uploadFile.txt" "name2" 34 | ContentType "text/json" 35 | } 36 | |> Request.toHttpRequestMessage 37 | 38 | -------------------------------------------------------------------------------- /fiddle/issue-178.fsx: -------------------------------------------------------------------------------- 1 | 2 | #r "../src/FsHttp/bin/debug/net6.0/FsHttp.dll" 3 | 4 | open System.IO 5 | open System.Net.Http 6 | open System.Net.Http.Headers 7 | open FsHttp 8 | open FsHttp.Operators 9 | 10 | 11 | let httpd0 = 12 | http { 13 | config_transformHeader (fun (header: Header) -> 14 | printfn "header.target: %A" header.target 15 | printfn "header.target.address: %A" header.target.address 16 | 17 | let address = "http://aaaa:5000" (header.target.address |> Option.defaultValue "") 18 | { header with target.address = Some address }) 19 | } 20 | 21 | 22 | 23 | let httpd1 = 24 | http { 25 | config_transformUrl (fun url -> "http://aaaa:5000" url) 26 | } 27 | 28 | let httpd2 = 29 | http { 30 | config_useBaseUrl "http://aaaa:5000" 31 | } 32 | -------------------------------------------------------------------------------- /fiddle/pr-110-parsing-MediaTypeHeader.fsx: -------------------------------------------------------------------------------- 1 | 2 | open System.Text 3 | open System.Net.Http.Headers 4 | 5 | let ct = $"text/xxx; charset={Encoding.UTF8.WebName}" 6 | 7 | // ----- 8 | // tests 9 | // ----- 10 | 11 | // fails 12 | MediaTypeHeaderValue ct 13 | 14 | // works 15 | MediaTypeHeaderValue.Parse ct 16 | 17 | -------------------------------------------------------------------------------- /fiddle/prettyFsiIntegration.fsx: -------------------------------------------------------------------------------- 1 | #r "nuget: PrettyFsi" 2 | PrettyFsi.addPrinters(fsi, PrettyFsi.TableMode.Implicit) 3 | 4 | #r "nuget: FsHttp" 5 | open FsHttp 6 | open FsHttp.Operators 7 | 8 | 9 | % http { 10 | GET "https://api.github.com/users/ronaldschlenker" 11 | UserAgent "FsHttp" 12 | } 13 | 14 | 15 | 16 | // 17 | typeof.IsSZArray 18 | -------------------------------------------------------------------------------- /fiddle/pxlClockFsiMessage.fsx: -------------------------------------------------------------------------------- 1 | 2 | #r "../src/FsHttp/bin/debug/net6.0/FsHttp.dll" 3 | 4 | open FsHttp 5 | open FsHttp.Operators 6 | 7 | 8 | http { 9 | GET "https://www.wikipedia.de" 10 | } 11 | |> Request.send 12 | |> ignore 13 | -------------------------------------------------------------------------------- /fiddle/readme_and_index_md.fsx: -------------------------------------------------------------------------------- 1 | #r "nuget: FsHttp" 2 | 3 | open FsHttp 4 | 5 | http { 6 | POST "https://reqres.in/api/users" 7 | CacheControl "no-cache" 8 | body 9 | json """ 10 | { 11 | "name": "morpheus", 12 | "job": "leader" 13 | } 14 | """ 15 | } 16 | -------------------------------------------------------------------------------- /fiddle/utf8StreamStuff.fsx: -------------------------------------------------------------------------------- 1 | open System 2 | open System.IO 3 | open System.Text 4 | 5 | let readUtf8StringAsync maxLen (stream: Stream) = 6 | async { 7 | use reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks = false, bufferSize = 1024, leaveOpen = true) 8 | let sb = StringBuilder() 9 | let mutable codePointsRead = 0 10 | let buffer = Array.zeroCreate 1 // Buffer to read one character at a time 11 | while codePointsRead < maxLen && not reader.EndOfStream do 12 | // Read the next character asynchronously 13 | let! charsRead = reader.ReadAsync(buffer, 0, 1) |> Async.AwaitTask 14 | if charsRead > 0 then 15 | let c = buffer.[0] 16 | sb.Append(c) |> ignore 17 | // Check if the character is a high surrogate 18 | if Char.IsHighSurrogate(c) && not reader.EndOfStream then 19 | // Read the low surrogate asynchronously and append it 20 | let! nextCharsRead = reader.ReadAsync(buffer, 0, 1) |> Async.AwaitTask 21 | if nextCharsRead > 0 then 22 | let nextC = buffer.[0] 23 | sb.Append(nextC) |> ignore 24 | // Increment the code point count 25 | codePointsRead <- codePointsRead + 1 26 | return sb.ToString() 27 | } 28 | 29 | let text = "a😉b🙁🙂d" 30 | 31 | let test len (expected: string) = 32 | let res = 33 | new MemoryStream(Encoding.UTF8.GetBytes(text)) 34 | |> readUtf8StringAsync len 35 | |> Async.RunSynchronously 36 | let s1 = Encoding.UTF8.GetBytes res |> Array.toList 37 | let s2 = Encoding.UTF8.GetBytes expected |> Array.toList 38 | let res = (s1 = s2) 39 | if not res then 40 | printfn "" 41 | printfn "count = %d" len 42 | printfn "expected = %s" expected 43 | printfn "" 44 | printfn "Expected: %A" s2 45 | printfn "" 46 | printfn "Actual : %A" s1 47 | printfn "" 48 | printfn " ----------------------------" 49 | 50 | test 0 "" 51 | test 1 "a" 52 | test 2 "a😉" 53 | test 3 "a😉b" 54 | test 4 "a😉b🙁" 55 | test 5 "a😉b🙁🙂" 56 | test 6 "a😉b🙁🙂d" 57 | -------------------------------------------------------------------------------- /fiddle/vscode_restclient.fsx: -------------------------------------------------------------------------------- 1 | #r "nuget: FsHttp" 2 | 3 | open System 4 | open FsHttp 5 | open FsHttp.Operators 6 | 7 | type Issue = { 8 | url: string 9 | repository_url: string 10 | labels_url: string 11 | comments_url: string 12 | events_url: string 13 | html_url: string 14 | id: int 15 | node_id: string 16 | number: int 17 | title: string 18 | user: {| 19 | login: string 20 | id: int 21 | node_id: string 22 | url: string 23 | html_url: string 24 | followers_url: string 25 | following_url: string 26 | gists_url: string 27 | starred_url: string 28 | subscriptions_url: string 29 | organizations_url: string 30 | repos_url: string 31 | events_url: string 32 | received_events_url: string 33 | ``type``: string 34 | site_admin: bool 35 | |} 36 | labels: string list 37 | state: string 38 | locked: bool 39 | assignee: string 40 | assignees: string list 41 | milestone: string 42 | comments: int 43 | created_at: DateTimeOffset 44 | updated_at: DateTimeOffset option 45 | closed_at: DateTimeOffset option 46 | author_association: string 47 | active_lock_reason: string 48 | body: string 49 | timeline_url: string 50 | performed_via_github_app: string 51 | state_reason: string 52 | } 53 | 54 | let githubGet route = 55 | http { 56 | GET ("https://api.github.com" route) 57 | AuthorizationBearer "**************" 58 | Accept "application/vnd.github.v3+json" 59 | UserAgent "FsHttp" 60 | header "X-GitHub-Api-Version" "2022-11-28" 61 | } 62 | 63 | let getIssues (repoOwner: string) (repo: string) = 64 | githubGet $"repos/{repoOwner}/{repo}/issues" 65 | |> Request.send 66 | |> Response.deserializeJson 67 | 68 | getIssues "vide-collabo" "vide" 69 | |> List.filter (fun x -> x.closed_at.IsNone) 70 | |> List.map (fun x -> {| title = x.title; user = x.user.login |}) 71 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.0", 4 | "allowPrerelease": false, 5 | "rollForward": "latestMinor" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | dotnet fsi build.fsx publish -------------------------------------------------------------------------------- /src/FsHttp.FSharpData/FsHttp.FSharpData.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | false 5 | Debug;Release 6 | true 7 | FSharp.Data (JSON) integration package for FsHttp 8 | FsHttp.FSharpData 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/FsHttp.FSharpData/JsonComparison.fs: -------------------------------------------------------------------------------- 1 | namespace FsHttp.FSharpData 2 | 3 | open System 4 | open FsHttp.Helper 5 | open FSharp.Data 6 | 7 | [] 8 | module JsonComparisonTypes = 9 | type ArrayComparison = 10 | | RespectOrder 11 | | IgnoreOrder 12 | 13 | type StructuralComparison = 14 | | Subset 15 | | Exact 16 | 17 | module Json = 18 | let compareJson (arrayComparison: ArrayComparison) (expectedJson: JsonValue) (resultJson: JsonValue) = 19 | let rec toPaths (currentPath: string) (jsonValue: JsonValue) : ((string * obj) list) = 20 | match jsonValue with 21 | | JsonValue.Null -> [ currentPath, null :> obj ] 22 | | JsonValue.Record properties -> 23 | seq { 24 | for pName, pValue in properties do 25 | for innerPath in toPaths (sprintf "%s/%s" currentPath pName) pValue do 26 | yield innerPath 27 | } 28 | |> Seq.toList 29 | | JsonValue.Array values -> 30 | let indexedValues = values |> Array.mapi (fun i x -> i, x) 31 | 32 | seq { 33 | for index, value in indexedValues do 34 | let printedIndex = 35 | match arrayComparison with 36 | | RespectOrder -> index.ToString() 37 | | IgnoreOrder -> "" 38 | 39 | for inner in toPaths (sprintf "%s[%s]" currentPath printedIndex) value do 40 | yield inner 41 | } 42 | |> Seq.toList 43 | | JsonValue.Boolean b -> [ currentPath, b :> obj ] 44 | | JsonValue.Float f -> [ currentPath, f :> obj ] 45 | | JsonValue.String s -> [ currentPath, s :> obj ] 46 | | JsonValue.Number n -> [ currentPath, n :> obj ] 47 | 48 | let getPaths x = x |> toPaths "" |> List.map (fun (path, value) -> sprintf "%s{%A}" path value) 49 | (getPaths expectedJson, getPaths resultJson) 50 | 51 | let expectJson 52 | (arrayComparison: ArrayComparison) 53 | (structuralComparison: StructuralComparison) 54 | (expectedJson: string) 55 | (actualJsonValue: JsonValue) 56 | = 57 | let expectedPaths, resultPaths = 58 | compareJson arrayComparison (JsonValue.Parse expectedJson) actualJsonValue 59 | 60 | let aggregateUnmatchedElements list = 61 | match list with 62 | | [] -> "" 63 | | x :: xs -> xs |> List.fold (fun curr next -> curr + "\n" + next) x 64 | 65 | match structuralComparison with 66 | | Subset -> 67 | let eMinusR = expectedPaths |> List.except resultPaths 68 | 69 | match eMinusR with 70 | | [] -> Ok actualJsonValue 71 | | _ -> Error(sprintf "Elements not contained in source: \n%s" (eMinusR |> aggregateUnmatchedElements)) 72 | | Exact -> 73 | let eMinusR = expectedPaths |> List.except resultPaths 74 | let rMinusE = resultPaths |> List.except expectedPaths 75 | 76 | match eMinusR, rMinusE with 77 | | [], [] -> Ok actualJsonValue 78 | | _ -> 79 | let a1 = 80 | (sprintf "Elements not contained in source: \n%s" (eMinusR |> aggregateUnmatchedElements)) 81 | 82 | let a2 = 83 | (sprintf "Elements not contained in expectation: \n%s" (rMinusE |> aggregateUnmatchedElements)) 84 | 85 | Error(sprintf "%s\n%s" a1 a2) 86 | 87 | let expectJsonSubset (expectedJson: string) (actualJsonValue: JsonValue) = 88 | expectJson IgnoreOrder Subset expectedJson actualJsonValue 89 | 90 | let expectJsonExact (expectedJson: string) (actualJsonValue: JsonValue) = 91 | expectJson IgnoreOrder Exact expectedJson actualJsonValue 92 | |> Result.getValueOrThrow Exception 93 | 94 | 95 | // ----------- 96 | // Assert 97 | // ----------- 98 | let assertJson 99 | (arrayComparison: ArrayComparison) 100 | (structuralComparison: StructuralComparison) 101 | (expectedJson: string) 102 | (actualJsonValue: JsonValue) 103 | = 104 | expectJson arrayComparison structuralComparison expectedJson actualJsonValue 105 | |> Result.getValueOrThrow Exception 106 | 107 | let assertJsonSubset (expectedJson: string) (actualJsonValue: JsonValue) = 108 | assertJson IgnoreOrder Subset expectedJson actualJsonValue 109 | 110 | let assertJsonExact (expectedJson: string) (actualJsonValue: JsonValue) = 111 | assertJson IgnoreOrder Exact expectedJson actualJsonValue 112 | -------------------------------------------------------------------------------- /src/FsHttp.FSharpData/JsonExtensions.fs: -------------------------------------------------------------------------------- 1 | namespace FsHttp.FSharpData 2 | 3 | open FSharp.Data 4 | 5 | [] 6 | module JsonExtensions = 7 | type JsonValue with 8 | member this.HasProperty(propertyName: string) = 9 | let prop = this.TryGetProperty propertyName 10 | 11 | match prop with 12 | | Some _ -> true 13 | | None -> false 14 | -------------------------------------------------------------------------------- /src/FsHttp.FSharpData/Response.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.FSharpData.Response 2 | 3 | open System.IO 4 | open FsHttp 5 | open FsHttp.Helper 6 | open FSharp.Data 7 | 8 | let toJsonAsync response = 9 | response 10 | |> Response.parseAsync 11 | "JSON" 12 | (fun stream ct -> 13 | async { 14 | use sr = new StreamReader(stream) 15 | let! s = sr.ReadToEndAsync() |> Async.AwaitTask 16 | return JsonValue.Parse s 17 | } 18 | ) 19 | let toJsonTAsync cancellationToken response = 20 | Async.StartAsTask( 21 | toJsonAsync response, 22 | cancellationToken = cancellationToken) 23 | let toJson response = 24 | toJsonAsync response |> Async.RunSynchronously 25 | 26 | let toJsonArrayAsync response = 27 | async { 28 | let! res = toJsonAsync response 29 | return res.AsArray() 30 | } 31 | let toJsonArrayTAsync cancellationToken response = 32 | Async.StartAsTask( 33 | toJsonArrayAsync response, 34 | cancellationToken = cancellationToken) 35 | let toJsonArray response = 36 | toJsonArrayAsync response |> Async.RunSynchronously 37 | 38 | let toJsonListAsync response = 39 | async { 40 | let! res = toJsonAsync response 41 | return res.AsArray() |> Array.toList 42 | } 43 | let toJsonListTAsync cancellationToken response = 44 | Async.StartAsTask( 45 | toJsonListAsync response, 46 | cancellationToken = cancellationToken) 47 | let toJsonList response = 48 | toJsonListAsync response |> Async.RunSynchronously 49 | -------------------------------------------------------------------------------- /src/FsHttp.NewtonsoftJson/FsHttp.NewtonsoftJson.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | false 5 | Debug;Release 6 | true 7 | 8 | 9 | JSON.Net (Newtonsoft.Json) integration package for FsHttp 10 | FsHttp.NewtonsoftJson 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/FsHttp.NewtonsoftJson/GlobalConfig.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.NewtonsoftJson.GlobalConfig 2 | 3 | open Newtonsoft.Json 4 | open Newtonsoft.Json.Linq 5 | 6 | module Json = 7 | let mutable defaultJsonLoadSettings = JsonLoadSettings() 8 | let mutable defaultJsonSerializerSettings = JsonSerializerSettings() 9 | -------------------------------------------------------------------------------- /src/FsHttp.NewtonsoftJson/Operators.fs: -------------------------------------------------------------------------------- 1 | namespace FsHttp.NewtonsoftJson 2 | 3 | open Newtonsoft.Json.Linq 4 | 5 | [] 6 | module JsonDynamic = 7 | let (?) (json: JToken) (key: string) : JToken = json[key] 8 | -------------------------------------------------------------------------------- /src/FsHttp.NewtonsoftJson/Response.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.NewtonsoftJson.Response 2 | 3 | open System.IO 4 | 5 | open FsHttp 6 | open FsHttp.Helper 7 | open FsHttp.NewtonsoftJson.GlobalConfig.Json 8 | 9 | open Newtonsoft.Json 10 | open Newtonsoft.Json.Linq 11 | 12 | // ----------------------------- 13 | // JSON.Net doesn't really support async - but we try to keep the API in sync with System.Text.Json 14 | // ----------------------------- 15 | 16 | let private loadJsonAsync loader response = 17 | response 18 | |> Response.parseAsync 19 | "JSON" 20 | (fun stream ct -> 21 | // Don't dispose sr and jr - the incoming stream is owned externally! 22 | let sr = new StreamReader(stream) 23 | let jr = new JsonTextReader(sr) 24 | loader jr ct |> Async.AwaitTask 25 | ) 26 | 27 | let toJsonWithAsync settings response = response |> loadJsonAsync (fun jr ct -> JObject.LoadAsync(jr, settings, ct)) 28 | let toJsonWithTAsync settings cancellationToken response = 29 | Async.StartAsTask( 30 | toJsonWithAsync settings response, 31 | cancellationToken = cancellationToken) 32 | let toJsonWith settings response = toJsonWithAsync settings response |> Async.RunSynchronously 33 | 34 | let toJsonAsync response = 35 | toJsonWithAsync defaultJsonLoadSettings response 36 | let toJsonTAsync cancellationToken response = 37 | toJsonWithTAsync defaultJsonLoadSettings cancellationToken response 38 | let toJson response = 39 | toJsonWith defaultJsonLoadSettings response 40 | 41 | let toJsonSeqWithAsync settings response = 42 | response 43 | |> loadJsonAsync (fun jr ct -> JArray.LoadAsync(jr, settings, ct)) 44 | |> Async.map (fun jarr -> jarr :> JToken seq) 45 | 46 | let toJsonSeqWithTAsync settings cancellationToken response = 47 | Async.StartAsTask( 48 | toJsonSeqWithAsync settings response, 49 | cancellationToken = cancellationToken) 50 | let toJsonSeqWith settings response = 51 | toJsonSeqWithAsync settings response |> Async.RunSynchronously 52 | 53 | let toJsonSeqAsync response = 54 | toJsonSeqWithAsync defaultJsonLoadSettings response 55 | let toJsonSeqTAsync cancellationToken response = 56 | toJsonSeqWithTAsync defaultJsonLoadSettings cancellationToken response 57 | let toJsonSeq response = 58 | toJsonSeqWith defaultJsonLoadSettings response 59 | 60 | let toJsonArrayWithAsync settings response = 61 | toJsonSeqWithAsync settings response |> Async.map Seq.toArray 62 | let toJsonArrayWithTAsync settings cancellationToken response = 63 | Async.StartAsTask( 64 | toJsonArrayWithAsync settings response, 65 | cancellationToken = cancellationToken) 66 | let toJsonArrayWith settings response = 67 | toJsonArrayWithAsync settings response |> Async.RunSynchronously 68 | 69 | let toJsonArrayAsync response = 70 | toJsonArrayWithAsync defaultJsonLoadSettings response 71 | let toJsonArrayTAsync cancellationToken response = 72 | toJsonArrayWithTAsync defaultJsonLoadSettings cancellationToken response 73 | let toJsonArray response = 74 | toJsonArrayWith defaultJsonLoadSettings response 75 | 76 | let toJsonListWithAsync settings response = 77 | toJsonSeqWithAsync settings response |> Async.map Seq.toList 78 | let toJsonListWithTAsync settings cancellationToken response = 79 | Async.StartAsTask( 80 | toJsonListWithAsync settings response, 81 | cancellationToken = cancellationToken) 82 | let toJsonListWith settings response = 83 | toJsonListWithAsync settings response |> Async.RunSynchronously 84 | 85 | let toJsonListAsync response = 86 | toJsonListWithAsync defaultJsonLoadSettings response 87 | let toJsonListTAsync cancellationToken response = 88 | toJsonListWithTAsync defaultJsonLoadSettings cancellationToken response 89 | let toJsonList response = 90 | toJsonListWith defaultJsonLoadSettings response 91 | 92 | let deserializeJsonWithAsync<'a> (settings: JsonSerializerSettings) response = 93 | async { 94 | let json = Response.toText response 95 | return JsonConvert.DeserializeObject<'a>(json, settings) 96 | } 97 | let deserializeWithJsonTAsync<'a> settings cancellationToken response = 98 | Async.StartAsTask( 99 | deserializeJsonWithAsync<'a> settings response, 100 | cancellationToken = cancellationToken) 101 | let deserializeWithJson<'a> settings response = 102 | deserializeJsonWithAsync<'a> settings response |> Async.RunSynchronously 103 | 104 | let deserializeJsonAsync<'a> response = 105 | deserializeJsonWithAsync<'a> defaultJsonSerializerSettings response 106 | let deserializeJsonTAsync<'a> cancellationToken response = 107 | deserializeWithJsonTAsync<'a> defaultJsonSerializerSettings cancellationToken response 108 | let deserializeJson<'a> response = 109 | deserializeWithJson<'a> defaultJsonSerializerSettings response 110 | -------------------------------------------------------------------------------- /src/FsHttp/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | module AssemblyInfo 2 | 3 | open System.Runtime.CompilerServices 4 | 5 | [] 6 | do () 7 | -------------------------------------------------------------------------------- /src/FsHttp/Autos.fs: -------------------------------------------------------------------------------- 1 | namespace FsHttp 2 | 3 | open System.Text.Json 4 | open System.Runtime.CompilerServices 5 | 6 | [] 7 | module SystemTextJsonExtensions = 8 | 9 | [] 10 | type JsonElementExtensions = 11 | 12 | // Is that a good thing? I don't know... Maybe not. 13 | [] 14 | static member ObjValue(this: JsonElement) : obj = 15 | let fromTry f = 16 | let succ, value = f () 17 | if succ then Some(value :> obj) else None 18 | 19 | match this.ValueKind with 20 | | JsonValueKind.True -> true 21 | | JsonValueKind.False -> false 22 | | JsonValueKind.Number -> 23 | fromTry this.TryGetInt32 24 | |> Option.orElse (fromTry this.TryGetInt64) 25 | |> Option.orElse (fromTry this.TryGetDouble) 26 | |> Option.defaultValue "" 27 | | JsonValueKind.Array -> 28 | this.EnumerateArray() 29 | |> Seq.toList 30 | :> obj 31 | | JsonValueKind.Null -> null 32 | | _ -> this.ToString() 33 | 34 | [] 35 | static member GetListOf<'a>(this: JsonElement) = 36 | this.EnumerateArray() 37 | |> Seq.map (fun x -> JsonElementExtensions.ObjValue(x) :?> 'a) 38 | |> Seq.toList 39 | 40 | [] 41 | static member GetList(this: JsonElement) = 42 | this.EnumerateArray() 43 | |> Seq.toList 44 | 45 | [] 46 | type JsonPropertyExtensions = 47 | 48 | [] 49 | static member ObjValue(this: JsonProperty) = 50 | JsonElementExtensions.ObjValue(this.Value) 51 | -------------------------------------------------------------------------------- /src/FsHttp/Defaults.fs: -------------------------------------------------------------------------------- 1 | module internal FsHttp.Defaults 2 | 3 | open System 4 | open System.Net 5 | open System.Net.Http 6 | 7 | open FsHttp 8 | open System.Text.Json 9 | 10 | let defaultJsonDocumentOptions = JsonDocumentOptions() 11 | let defaultJsonSerializerOptions = JsonSerializerOptions JsonSerializerDefaults.Web 12 | 13 | let defaultHttpClientFactory (config: Config) = 14 | let handler = 15 | new SocketsHttpHandler( 16 | UseCookies = false, 17 | PooledConnectionLifetime = TimeSpan.FromMinutes 5.0) 18 | let ignoreSslIssues = 19 | match config.certErrorStrategy with 20 | | Default -> false 21 | | AlwaysAccept -> true 22 | if ignoreSslIssues then 23 | do handler.SslOptions <- 24 | let options = Security.SslClientAuthenticationOptions() 25 | let callback = Security.RemoteCertificateValidationCallback(fun sender cert chain errors -> true) 26 | do options.RemoteCertificateValidationCallback <- callback 27 | options 28 | do handler.AutomaticDecompression <- 29 | config.defaultDecompressionMethods 30 | |> List.fold (fun c n -> c ||| n) DecompressionMethods.None 31 | let handler = config.httpClientHandlerTransformers |> List.fold (fun c n -> n c) handler 32 | 33 | match config.proxy with 34 | | Some proxy -> 35 | let webProxy = WebProxy(proxy.url) 36 | 37 | match proxy.credentials with 38 | | Some cred -> 39 | webProxy.UseDefaultCredentials <- false 40 | webProxy.Credentials <- cred 41 | | None -> webProxy.UseDefaultCredentials <- true 42 | 43 | handler.Proxy <- webProxy 44 | | None -> () 45 | 46 | let client = new HttpClient(handler) 47 | do config.timeout |> Option.iter (fun timeout -> client.Timeout <- timeout) 48 | client 49 | 50 | let defaultHeadersAndBodyPrintMode = { 51 | format = true 52 | maxLength = Some 7000 53 | } 54 | 55 | let defaultDecompressionMethods = [ DecompressionMethods.All ] 56 | 57 | let defaultConfig = 58 | { 59 | timeout = None 60 | defaultDecompressionMethods = defaultDecompressionMethods 61 | headerTransformers = [] 62 | httpMessageTransformers = [] 63 | httpClientHandlerTransformers = [] 64 | httpClientTransformers = [] 65 | httpClientFactory = defaultHttpClientFactory 66 | httpCompletionOption = HttpCompletionOption.ResponseHeadersRead 67 | proxy = None 68 | certErrorStrategy = Default 69 | bufferResponseContent = false 70 | cancellationToken = Threading.CancellationToken.None 71 | } 72 | 73 | let defaultPrintHint = 74 | { 75 | requestPrintMode = HeadersAndBody(defaultHeadersAndBodyPrintMode) 76 | responsePrintMode = HeadersAndBody(defaultHeadersAndBodyPrintMode) 77 | } 78 | -------------------------------------------------------------------------------- /src/FsHttp/Domain.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FsHttp.Domain 3 | 4 | open System.Threading 5 | 6 | type StatusCodeExpectation = { 7 | expected: System.Net.HttpStatusCode list 8 | actual: System.Net.HttpStatusCode 9 | } 10 | 11 | exception StatusCodeExpectedxception of StatusCodeExpectation 12 | 13 | type BodyPrintMode = { 14 | format: bool 15 | maxLength: int option 16 | } 17 | 18 | type PrintMode<'bodyPrintMode> = 19 | | AsObject 20 | | HeadersOnly 21 | | HeadersAndBody of BodyPrintMode 22 | 23 | type PrintHint = { 24 | requestPrintMode: PrintMode 25 | responsePrintMode: PrintMode 26 | } 27 | 28 | type CertErrorStrategy = 29 | | Default 30 | | AlwaysAccept 31 | 32 | type Proxy = { 33 | url: string 34 | credentials: System.Net.ICredentials option 35 | } 36 | 37 | type Config = { 38 | timeout: System.TimeSpan option 39 | defaultDecompressionMethods: System.Net.DecompressionMethods list 40 | headerTransformers: list
Header> 41 | httpMessageTransformers: list System.Net.Http.HttpRequestMessage> 42 | httpClientHandlerTransformers: list System.Net.Http.SocketsHttpHandler> 43 | httpClientTransformers: list System.Net.Http.HttpClient> 44 | httpCompletionOption: System.Net.Http.HttpCompletionOption 45 | proxy: Proxy option 46 | certErrorStrategy: CertErrorStrategy 47 | httpClientFactory: Config -> System.Net.Http.HttpClient 48 | // Calls `LoadIntoBufferAsync` of the response's HttpContent immediately after receiving. 49 | bufferResponseContent: bool 50 | cancellationToken: CancellationToken 51 | } 52 | 53 | and ConfigTransformer = Config -> Config 54 | 55 | and PrintHintTransformer = PrintHint -> PrintHint 56 | 57 | and FsHttpTarget = { 58 | method: System.Net.Http.HttpMethod option 59 | address: string option 60 | additionalQueryParams: list 61 | } 62 | 63 | and Header = { 64 | target: FsHttpTarget 65 | headers: Map 66 | // We use a .Net type here, which we never do in other places. 67 | // Since Cookie is record style, I see no problem here. 68 | cookies: System.Net.Cookie list 69 | } 70 | 71 | type BodyContent = 72 | | Empty 73 | | Single of SinglepartContent 74 | | Multi of MultipartContent 75 | 76 | and SinglepartContent = { 77 | contentElement: ContentElement 78 | headers: Map 79 | } 80 | 81 | and MultipartContent = { 82 | partElements: MultipartElement list 83 | headers: Map 84 | } 85 | 86 | and MultipartElement = { 87 | name: string 88 | content: ContentElement 89 | fileName: string option 90 | } 91 | 92 | and ContentData = 93 | | TextContent of string 94 | | BinaryContent of byte array 95 | | StreamContent of System.IO.Stream 96 | | FormUrlEncodedContent of Map 97 | | FileContent of string 98 | 99 | and ContentType = { 100 | value: string 101 | charset: System.Text.Encoding option 102 | } 103 | 104 | and ContentElement = { 105 | contentData: ContentData 106 | explicitContentType: ContentType option 107 | } 108 | 109 | type Request = { 110 | header: Header 111 | content: BodyContent 112 | config: Config 113 | printHint: PrintHint 114 | } 115 | 116 | type IToRequest = 117 | abstract member ToRequest: unit -> Request 118 | 119 | type IUpdateConfig<'self> = 120 | abstract member UpdateConfig: (Config -> Config) -> 'self 121 | 122 | type IUpdatePrintHint<'self> = 123 | abstract member UpdatePrintHint: (PrintHint -> PrintHint) -> 'self 124 | 125 | // It seems to impossible extending builder methods on the context type 126 | // directly when they are not polymorph. 127 | type IRequestContext<'self> = 128 | abstract member Self: 'self 129 | 130 | // Unifying IToBodyContext and IToMultipartContext doesn't work; see: 131 | // https://github.com/dotnet/fsharp/issues/12814 132 | type IToBodyContext = 133 | inherit IToRequest 134 | abstract member ToBodyContext: unit -> BodyContext 135 | 136 | and IToMultipartContext = 137 | inherit IToRequest 138 | abstract member ToMultipartContext: unit -> MultipartContext 139 | 140 | and HeaderContext = { 141 | header: Header 142 | config: Config 143 | printHint: PrintHint 144 | } with 145 | interface IRequestContext with 146 | member this.Self = this 147 | 148 | interface IUpdateConfig with 149 | member this.UpdateConfig(transformConfig) = 150 | { this with config = transformConfig this.config } 151 | 152 | interface IUpdatePrintHint with 153 | member this.UpdatePrintHint(transformPrintHint) = 154 | { this with printHint = transformPrintHint this.printHint } 155 | 156 | interface IToRequest with 157 | member this.ToRequest() = { 158 | header = this.header 159 | content = Empty 160 | config = this.config 161 | printHint = this.printHint 162 | } 163 | 164 | interface IToBodyContext with 165 | member this.ToBodyContext() = { 166 | header = this.header 167 | bodyContent = { 168 | contentElement = { 169 | contentData = BinaryContent [||] 170 | explicitContentType = None 171 | } 172 | headers = Map.empty 173 | } 174 | config = this.config 175 | printHint = this.printHint 176 | } 177 | 178 | interface IToMultipartContext with 179 | member this.ToMultipartContext() = { 180 | header = this.header 181 | config = this.config 182 | printHint = this.printHint 183 | multipartContent = { 184 | partElements = [] 185 | headers = Map.empty 186 | } 187 | } 188 | 189 | and BodyContext = { 190 | header: Header 191 | bodyContent: SinglepartContent 192 | config: Config 193 | printHint: PrintHint 194 | } with 195 | interface IRequestContext with 196 | member this.Self = this 197 | 198 | interface IUpdateConfig with 199 | member this.UpdateConfig(transformConfig) = 200 | { this with config = transformConfig this.config } 201 | 202 | interface IUpdatePrintHint with 203 | member this.UpdatePrintHint(transformPrintHint) = 204 | { this with printHint = transformPrintHint this.printHint } 205 | 206 | interface IToRequest with 207 | member this.ToRequest() = { 208 | header = this.header 209 | content = Single this.bodyContent 210 | config = this.config 211 | printHint = this.printHint 212 | } 213 | 214 | interface IToBodyContext with 215 | member this.ToBodyContext() = this 216 | 217 | and MultipartContext = { 218 | header: Header 219 | multipartContent: MultipartContent 220 | config: Config 221 | printHint: PrintHint 222 | } with 223 | interface IRequestContext with 224 | member this.Self = this 225 | 226 | interface IUpdateConfig with 227 | member this.UpdateConfig(transformConfig) = 228 | { this with config = transformConfig this.config } 229 | 230 | interface IUpdatePrintHint with 231 | member this.UpdatePrintHint(transformPrintHint) = 232 | { this with printHint = transformPrintHint this.printHint } 233 | 234 | interface IToRequest with 235 | member this.ToRequest() = { 236 | header = this.header 237 | content = Multi this.multipartContent 238 | config = this.config 239 | printHint = this.printHint 240 | } 241 | 242 | interface IToMultipartContext with 243 | member this.ToMultipartContext() = this 244 | 245 | and MultipartElementContext = { 246 | parent: MultipartContext 247 | part: MultipartElement 248 | } with 249 | interface IRequestContext with 250 | member this.Self = this 251 | 252 | interface IUpdateConfig with 253 | member this.UpdateConfig(transformConfig) = 254 | { this with parent.config = this.parent.config |> transformConfig } 255 | 256 | interface IUpdatePrintHint with 257 | member this.UpdatePrintHint(transformPrintHint) = 258 | { this with parent.printHint = transformPrintHint this.parent.printHint } 259 | 260 | interface IToRequest with 261 | member this.ToRequest() = 262 | let parentWithSelf = (this :> IToMultipartContext).ToMultipartContext() 263 | (parentWithSelf :> IToRequest).ToRequest() 264 | 265 | interface IToMultipartContext with 266 | member this.ToMultipartContext() = 267 | let parentElementsAndSelf = this.parent.multipartContent.partElements @ [ this.part ] 268 | { this with parent.multipartContent.partElements = parentElementsAndSelf }.parent 269 | 270 | type Response = { 271 | request: Request 272 | requestMessage: System.Net.Http.HttpRequestMessage 273 | content: System.Net.Http.HttpContent 274 | headers: System.Net.Http.Headers.HttpResponseHeaders 275 | reasonPhrase: string 276 | statusCode: System.Net.HttpStatusCode 277 | version: System.Version 278 | printHint: PrintHint 279 | originalHttpRequestMessage: System.Net.Http.HttpRequestMessage 280 | originalHttpResponseMessage: System.Net.Http.HttpResponseMessage 281 | dispose: unit -> unit 282 | } with 283 | interface IUpdatePrintHint with 284 | member this.UpdatePrintHint(transformPrintHint) = 285 | { this with request.printHint = transformPrintHint this.request.printHint } 286 | 287 | 288 | interface System.IDisposable with 289 | member this.Dispose() = this.dispose () 290 | -------------------------------------------------------------------------------- /src/FsHttp/DomainExtensions.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FsHttp.DomainExtensions 3 | 4 | open System 5 | open System.Net.Http.Headers 6 | open FsHttp 7 | 8 | type FsHttpTarget with 9 | member this.ToUriStringWithDefault(defaultValue) = 10 | let queryParamsString = 11 | this.additionalQueryParams 12 | |> Seq.map (fun (k, v) -> $"""{k}={Uri.EscapeDataString $"{v}"}""") 13 | |> String.concat "&" 14 | 15 | match this.address with 16 | | None -> defaultValue 17 | | Some address -> 18 | let uri = UriBuilder(address) 19 | uri.Query <- 20 | match uri.Query, queryParamsString with 21 | | "", "" -> "" 22 | | s, "" -> s 23 | | "", q -> $"?{q}" 24 | | s, q -> $"{s}&{q}" 25 | uri.ToString() 26 | 27 | type ContentType with 28 | member this.ToMediaHeaderValue() = 29 | let mhv = MediaTypeHeaderValue.Parse(this.value) 30 | do this.charset |> Option.iter (fun charset -> mhv.CharSet <- charset.WebName) 31 | mhv 32 | -------------------------------------------------------------------------------- /src/FsHttp/FsHttp.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | false 5 | Debug;Release 6 | true 7 | 8 8 | true 9 | bin\$(Configuration)\$(TargetFramework)\FsHttp.xml 10 | A .Net HTTP client library for F#, C#, and friends 11 | FsHttp 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/FsHttp/Fsi.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Fsi 2 | 3 | // why we have this? -> search for #121 4 | let mutable internal logDebugMessages = None 5 | let enableDebugLogs () = logDebugMessages <- Some true 6 | let disableDebugLogs () = logDebugMessages <- Some false 7 | 8 | let logfn message = 9 | let logDebugMessages = logDebugMessages |> Option.defaultValue false 10 | 11 | message 12 | |> Printf.kprintf (fun s -> 13 | if logDebugMessages then 14 | printfn "%s" s 15 | ) 16 | -------------------------------------------------------------------------------- /src/FsHttp/FsiInit.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.FsiInit 2 | 3 | open System 4 | open FsHttp.Domain 5 | 6 | type InitResult = 7 | | Uninitialized 8 | | Initialized 9 | /// FSI object not found (this is expected when running in a notebook). 10 | | NoFsiObjectFound 11 | | IsNotInteractive 12 | | InitializationError of Exception 13 | 14 | let mutable private state = Uninitialized 15 | 16 | let private logPxlClockOnFirstSend = 17 | let mutable firstSend = true 18 | fun () -> 19 | if firstSend then 20 | firstSend <- false 21 | let msg = @" 22 | 23 | ************************************************************** 24 | 25 | +---------+ 26 | | | PXL-JAM 2024 27 | | PXL | - github.com/CuminAndPotato/PXL-JAM 28 | | CLOCK | - WIN a PXL-Clock MK1 29 | | | - until 8th of January 2025 30 | +---------+ 31 | 32 | ************************************************************** 33 | 34 | " 35 | printfn "%s" msg 36 | 37 | // This seems like a HACK, but there shouldn't be the requirement of referencing FCS in FSI. 38 | let doInit () = 39 | if state <> Uninitialized then 40 | state 41 | else 42 | let fsiAssemblyName = "FSI-ASSEMBLY" 43 | let isInteractive = 44 | // This hack is indeed one (see https://fsharp.github.io/fsharp-compiler-docs/fsi-emit.html) 45 | AppDomain.CurrentDomain.GetAssemblies() 46 | |> Array.map (fun asm -> asm.GetName().Name) 47 | |> Array.exists (fun asmName -> (*asm.IsDynamic &&*) 48 | asmName.StartsWith(fsiAssemblyName, StringComparison.Ordinal)) 49 | 50 | state <- 51 | try 52 | if isInteractive then 53 | AppDomain.CurrentDomain.GetAssemblies() 54 | |> Array.tryFind (fun x -> x.GetName().Name = "FSharp.Compiler.Interactive.Settings") 55 | |> Option.map (fun asm -> 56 | asm.ExportedTypes 57 | |> Seq.tryFind (fun t -> t.FullName = "FSharp.Compiler.Interactive.Settings") 58 | |> Option.map (fun settings -> 59 | settings.GetProperty("fsi") 60 | |> Option.ofObj 61 | |> Option.map (fun x -> x.GetValue(null)) 62 | ) 63 | ) 64 | |> Option.flatten 65 | |> Option.flatten 66 | |> function 67 | | None -> NoFsiObjectFound 68 | | Some fsiInstance -> 69 | do 70 | // see #121: It's important to not touch the logDebugMessages 71 | // value when it was already set before this init function was called. 72 | match Fsi.logDebugMessages with 73 | | None -> 74 | do logPxlClockOnFirstSend () 75 | Fsi.enableDebugLogs () 76 | | _ -> () 77 | 78 | let addPrinter (f: 'a -> string) = 79 | let t = fsiInstance.GetType() 80 | let addPrinterMethod = t.GetMethod("AddPrinter").MakeGenericMethod([| typeof<'a> |]) 81 | addPrinterMethod.Invoke(fsiInstance, [| f |]) |> ignore 82 | ////let addPrintTransformer = t.GetMethod("AddPrintTransformer").MakeGenericMethod([| typeof |]) 83 | ////let printTransformer (r: Response) = 84 | //// match r.request.config.printHint.isEnabled with 85 | //// | true -> (PrintableResponse r) :> obj 86 | //// | false -> null 87 | let printSafe f = 88 | try f () 89 | with ex -> ex.ToString() 90 | 91 | let responsePrinter (r: Response) = printSafe (fun () -> Response.print r) 92 | let requestPrinter (r: IToRequest) = printSafe (fun () -> Request.print r) 93 | do addPrinter responsePrinter 94 | do addPrinter requestPrinter 95 | ////addPrintTransformer.Invoke(fsiInstance, [| printTransformer |]) |> ignore 96 | Initialized 97 | else 98 | IsNotInteractive 99 | with ex -> 100 | InitializationError ex 101 | 102 | state 103 | 104 | let init () = 105 | doInit () |> ignore 106 | -------------------------------------------------------------------------------- /src/FsHttp/GlobalConfig.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.GlobalConfig 2 | 3 | open FsHttp.Domain 4 | 5 | let mutable private mutableDefaultConfig = Defaults.defaultConfig 6 | let mutable private mutableDefaultPrintHint = Defaults.defaultPrintHint 7 | 8 | // This thing enables config settings like in pipe style, but for the "global" config. 9 | type GlobalConfigWrapper(config: Config option, printHint: PrintHint option) = 10 | member _.Config = config |> Option.defaultValue mutableDefaultConfig 11 | member _.PrintHint = printHint |> Option.defaultValue mutableDefaultPrintHint 12 | 13 | interface IUpdateConfig with 14 | member this.UpdateConfig(transformConfig) = 15 | let updatedConfig = transformConfig this.Config 16 | GlobalConfigWrapper(Some updatedConfig, Some this.PrintHint) 17 | 18 | interface IUpdatePrintHint with 19 | member this.UpdatePrintHint(transformPrintHint) = 20 | let updatedConfig = transformPrintHint this.PrintHint 21 | GlobalConfigWrapper(Some this.Config, Some updatedConfig) 22 | 23 | let defaults = GlobalConfigWrapper(None, None) 24 | let set (config: GlobalConfigWrapper) = mutableDefaultConfig <- config.Config 25 | 26 | // TODO: Do we need something like this, which is more intuitive, but doesn't 27 | // support the pipelined config API? 28 | ////module Defaults = 29 | //// let get () = mutableDefaults 30 | //// let set (config: Config) = mutableDefaults <- config 31 | 32 | // TODO: Document this 33 | module Json = 34 | let mutable defaultJsonDocumentOptions = Defaults.defaultJsonDocumentOptions 35 | let mutable defaultJsonSerializerOptions = Defaults.defaultJsonSerializerOptions 36 | -------------------------------------------------------------------------------- /src/FsHttp/Helper.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Helper 2 | 3 | open System 4 | open System.IO 5 | open System.Text 6 | open System.Net.Http.Headers 7 | open System.Runtime.InteropServices 8 | open FsHttp 9 | 10 | [] 11 | module Encoding = 12 | let base64 = Encoding.GetEncoding("ISO-8859-1") 13 | 14 | [] 15 | module Async = 16 | let map f x = 17 | async { 18 | let! x = x 19 | return f x 20 | } 21 | 22 | type StringBuilder with 23 | member sb.append(s: string) = sb.Append s |> ignore 24 | member sb.appendLine(s: string) = sb.AppendLine s |> ignore 25 | member sb.newLine() = sb.appendLine "" 26 | 27 | member sb.appendSection(s: string) = 28 | sb.appendLine s 29 | 30 | String([ 0 .. s.Length ] |> List.map (fun _ -> '-') |> List.toArray) 31 | |> sb.appendLine 32 | 33 | [] 34 | module Map = 35 | let union (m1: Map<'k, 'v>) (s: seq<'k * 'v>) = 36 | seq { 37 | yield! m1 |> Seq.map (fun kvp -> kvp.Key, kvp.Value) 38 | yield! s 39 | } 40 | |> Map.ofSeq 41 | 42 | [] 43 | module Result = 44 | let getValueOrThrow ex (r: Result<'a, 'b>) = 45 | match r with 46 | | Ok value -> value 47 | | Error value -> raise (ex value) 48 | 49 | [] 50 | module String = 51 | let urlEncode (s: string) = System.Web.HttpUtility.UrlEncode(s) 52 | let toBase64 (s: string) = s |> Encoding.base64.GetBytes |> Convert.ToBase64String 53 | let fromBase64 (s: string) = s |> Convert.FromBase64String |> Encoding.base64.GetString 54 | let substring maxLength (s: string) = string (s.Substring(0, Math.Min(maxLength, s.Length))) 55 | 56 | [] 57 | module Url = 58 | let combine (url1: string) (url2: string) = 59 | let del = '/' 60 | let sdel = string del 61 | let norm (s: string) = s.Trim().Replace(@"\", sdel) 62 | let delTrim = [| del |] 63 | let a = (norm url1).TrimEnd(delTrim) 64 | let b = (norm url2).TrimStart(delTrim).TrimEnd(delTrim) 65 | a + sdel + b 66 | 67 | [] 68 | module Stream = 69 | let readUtf8StringAsync maxLen (stream: Stream) = 70 | // we could definitely optimize this 71 | async { 72 | use reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks = false, bufferSize = 1024, leaveOpen = true) 73 | let sb = StringBuilder() 74 | let mutable codePointsRead = 0 75 | let buffer = Array.zeroCreate 1 // Buffer to read one character at a time 76 | while codePointsRead < maxLen && not reader.EndOfStream do 77 | // Read the next character asynchronously 78 | let! charsRead = reader.ReadAsync(buffer, 0, 1) |> Async.AwaitTask 79 | if charsRead > 0 then 80 | let c = buffer.[0] 81 | sb.Append(c) |> ignore 82 | // Check if the character is a high surrogate 83 | if Char.IsHighSurrogate(c) && not reader.EndOfStream then 84 | // Read the low surrogate asynchronously and append it 85 | let! nextCharsRead = reader.ReadAsync(buffer, 0, 1) |> Async.AwaitTask 86 | if nextCharsRead > 0 then 87 | let nextC = buffer.[0] 88 | sb.Append(nextC) |> ignore 89 | // Increment the code point count 90 | codePointsRead <- codePointsRead + 1 91 | return sb.ToString() 92 | } 93 | 94 | let readUtf8StringTAsync maxLength (stream: Stream) = 95 | readUtf8StringAsync maxLength stream |> Async.StartAsTask 96 | 97 | let copyToCallbackAsync (target: Stream) callback (source: Stream) = 98 | async { 99 | let buffer = Array.create 1024 (byte 0) 100 | let logTimeSpan = TimeSpan.FromSeconds 1.5 101 | let mutable continueLooping = true 102 | let mutable overallBytesCount = 0 103 | let mutable lastNotificationTime = DateTime.Now 104 | 105 | while continueLooping do 106 | let! readCount = source.ReadAsync(buffer, 0, buffer.Length) |> Async.AwaitTask 107 | do target.Write(buffer, 0, readCount) 108 | do overallBytesCount <- overallBytesCount + readCount 109 | let now = DateTime.Now 110 | 111 | if (now - lastNotificationTime) > logTimeSpan then 112 | do callback overallBytesCount 113 | do lastNotificationTime <- now 114 | 115 | do continueLooping <- readCount > 0 116 | 117 | callback overallBytesCount 118 | } 119 | 120 | let copyToCallbackTAsync (target: Stream) callback (source: Stream) = 121 | copyToCallbackAsync target callback source |> Async.StartAsTask 122 | 123 | let copyToAsync target source = 124 | async { 125 | Fsi.logfn "Download response received - starting download..." 126 | 127 | do! 128 | source 129 | |> copyToCallbackAsync 130 | target 131 | (fun read -> 132 | let mbRead = float read / 1024.0 / 1024.0 133 | Fsi.logfn "%f MB" mbRead 134 | ) 135 | 136 | Fsi.logfn "Download finished." 137 | } 138 | 139 | let copyToTAsync target source = copyToAsync target source |> Async.StartAsTask 140 | 141 | let toStringUtf8Async source = 142 | async { 143 | use ms = new MemoryStream() 144 | do! source |> copyToAsync ms 145 | do ms.Position <- 0L 146 | use sr = new StreamReader(ms, Encoding.UTF8) 147 | return sr.ReadToEnd() 148 | } 149 | 150 | let toStringUtf8TAsync source = toStringUtf8Async source |> Async.StartAsTask 151 | 152 | let toBytesAsync source = 153 | async { 154 | use ms = new MemoryStream() 155 | do! source |> copyToAsync ms 156 | return ms.ToArray() 157 | } 158 | 159 | let toBytesTAsync source = toBytesAsync source |> Async.StartAsTask 160 | 161 | let saveFileAsync fileName source = 162 | async { 163 | Fsi.logfn "Download response received (file: %s) - starting download..." fileName 164 | use fs = File.Open(fileName, FileMode.Create, FileAccess.Write) 165 | do! source |> copyToAsync fs 166 | Fsi.logfn "Download finished." 167 | } 168 | 169 | let saveFileTAsync fileName source = saveFileAsync fileName source |> Async.StartAsTask 170 | 171 | 172 | type EnumerableStream(source: byte seq) = 173 | inherit Stream() 174 | 175 | let enumerator = source.GetEnumerator() 176 | let mutable isDisposed = false 177 | let mutable position = 0L 178 | 179 | override _.CanRead = true 180 | override _.CanSeek = false 181 | override _.CanWrite = false 182 | 183 | override _.Length = raise (NotSupportedException()) 184 | 185 | override _.Position 186 | with get() = position 187 | and set(_) = raise (NotSupportedException()) 188 | 189 | override _.Flush() = () 190 | 191 | override _.Read(buffer: byte[], offset: int, count: int) = 192 | let bytesToRead = Math.Min(count, buffer.Length - int position) 193 | if bytesToRead <= 0 then 0 194 | else 195 | let mutable bytesRead = 0 196 | while bytesRead < bytesToRead && enumerator.MoveNext() do 197 | buffer.[offset + bytesRead] <- enumerator.Current 198 | bytesRead <- bytesRead + 1 199 | position <- position + int64 bytesRead 200 | bytesRead 201 | 202 | override _.Seek(_: int64, _: SeekOrigin) = raise (NotSupportedException()) 203 | override _.SetLength(_: int64) = raise (NotSupportedException()) 204 | override _.Write(_: byte[], _: int, _: int) = raise (NotSupportedException()) 205 | 206 | override _.Dispose(disposing: bool) = 207 | if not isDisposed then 208 | isDisposed <- true 209 | enumerator.Dispose() 210 | -------------------------------------------------------------------------------- /src/FsHttp/Operators.fs: -------------------------------------------------------------------------------- 1 | namespace FsHttp 2 | 3 | open System.Text.Json 4 | 5 | [] 6 | module JsonDynamic = 7 | let (?) (json: JsonElement) (key: string) : JsonElement = json.GetProperty(key) 8 | 9 | module Operators = 10 | let () = Helper.Url.combine 11 | let (~%) = Request.send 12 | -------------------------------------------------------------------------------- /src/FsHttp/Print.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FsHttp.Print 3 | 4 | open System 5 | open System.Collections.Generic 6 | open System.Net.Http 7 | open System.Text 8 | 9 | open FsHttp 10 | open FsHttp.Helper 11 | 12 | let internal contentIndicator = "===content===" 13 | 14 | let private printHeaderCollection (headers: KeyValuePair seq) = 15 | let sb = StringBuilder() 16 | 17 | let maxHeaderKeyLength = 18 | let lengths = headers |> Seq.map (fun h -> h.Key.Length) |> Seq.toList 19 | 20 | match lengths with 21 | | [] -> 0 22 | | list -> list |> Seq.max 23 | 24 | for h in headers do 25 | let values = String.Join(", ", h.Value) 26 | do sb.appendLine (sprintf "%-*s: %s" (maxHeaderKeyLength + 3) h.Key values) 27 | 28 | sb.ToString() 29 | 30 | let private doPrintRequestOnly (httpVersion: string) (request: Request) (requestMessage: HttpRequestMessage) = 31 | let sb = StringBuilder() 32 | let requestPrintHint = request.printHint.requestPrintMode 33 | 34 | do sb.appendSection "REQUEST" 35 | do sb.appendLine $"{Request.addressToString request} HTTP/{httpVersion}" 36 | 37 | let printRequestHeaders () = 38 | let contentHeaders, multipartHeaders = 39 | if not (isNull requestMessage.Content) then 40 | let a = requestMessage.Content.Headers |> Seq.toList 41 | 42 | let b = 43 | match requestMessage.Content with 44 | | :? MultipartFormDataContent as m -> 45 | // TODO: After having the request invoked, the dotnet multiparts 46 | // have no headers anymore... 47 | m |> Seq.collect (fun part -> part.Headers) |> Seq.toList 48 | | _ -> [] 49 | 50 | a, b 51 | else 52 | [], [] 53 | 54 | sb.append 55 | <| printHeaderCollection ((requestMessage.Headers |> Seq.toList) @ contentHeaders @ multipartHeaders) 56 | 57 | let printRequestBody () = 58 | let formatContentData contentData = 59 | match contentData with 60 | | TextContent s -> s 61 | | BinaryContent bytes -> sprintf "::Binary (length = %d)" bytes.Length 62 | | StreamContent stream -> 63 | sprintf "::Stream (length = %s)" (if stream.CanSeek then stream.Length.ToString() else "?") 64 | | FormUrlEncodedContent formDataList -> 65 | [ 66 | yield "::FormUrlEncoded" 67 | for kvp in formDataList do 68 | yield sprintf " %s = %s" kvp.Key kvp.Value 69 | ] 70 | |> String.concat "\n" 71 | | FileContent fileName -> sprintf "::File (name = %s)" fileName 72 | 73 | let multipartIndicator = 74 | match request.content with 75 | | Multi _ -> " :: Multipart" 76 | | _ -> "" 77 | 78 | sb.appendLine (contentIndicator + multipartIndicator) 79 | 80 | match request.content with 81 | | Empty -> "" 82 | | Single bodyContent -> formatContentData bodyContent.contentElement.contentData 83 | | Multi multipartContent -> 84 | [ 85 | for part in multipartContent.partElements do 86 | yield $"-------- {part.name}" 87 | 88 | match part.content.explicitContentType with 89 | | Some x -> yield $"Part content type: {x.ToMediaHeaderValue().ToString()}" 90 | | _ -> () 91 | 92 | yield formatContentData part.content.contentData 93 | ] 94 | |> String.concat "\n" 95 | |> sb.appendLine 96 | 97 | 98 | // TODO: bodyConfig 99 | match requestPrintHint with 100 | | AsObject -> sb.appendLine (sprintf "%A" request) 101 | | HeadersOnly -> printRequestHeaders () 102 | | HeadersAndBody bodyConfig -> 103 | printRequestHeaders () 104 | printRequestBody () 105 | 106 | sb.newLine () 107 | sb.ToString() 108 | 109 | let private printRequestOnly (request: IToRequest) = 110 | let request, requestMessage = request |> Request.toRequestAndMessage 111 | doPrintRequestOnly "?" request requestMessage 112 | 113 | let private printResponseOnly (response: Response) = 114 | let sb = StringBuilder() 115 | 116 | sb.appendSection "RESPONSE" 117 | 118 | sb.appendLine ( 119 | sprintf "HTTP/%s %d %s" (response.version.ToString()) (int response.statusCode) (string response.statusCode) 120 | ) 121 | 122 | //if r.request.config.printHint.responsePrintMode.printHeader then 123 | let printResponseHeaders () = 124 | let allHeaders = 125 | (response.headers |> Seq.toList) @ (response.content.Headers |> Seq.toList) 126 | 127 | sb.appendLine (printHeaderCollection allHeaders) 128 | 129 | //if r.request.config.printHint.responsePrintMode.printContent.isEnabled then 130 | let printResponseBody (format: bool) (maxLength: int option) = 131 | let trimmedContentText = 132 | try 133 | let contentText = 134 | if format then 135 | Response.toFormattedText response 136 | else 137 | Response.toText response 138 | 139 | match maxLength with 140 | | Some maxLength when contentText.Length > maxLength -> 141 | (contentText.Substring(0, maxLength)) + $"{Environment.NewLine}..." 142 | | _ -> contentText 143 | with ex -> 144 | sprintf "ERROR reading response content: %s" (ex.ToString()) 145 | 146 | sb.appendLine contentIndicator 147 | sb.append trimmedContentText 148 | 149 | match response.request.printHint.responsePrintMode with 150 | | AsObject -> sb.appendLine (sprintf "%A" response) 151 | | HeadersOnly -> printResponseHeaders () 152 | | HeadersAndBody bodyConfig -> 153 | printResponseHeaders () 154 | printResponseBody bodyConfig.format bodyConfig.maxLength 155 | 156 | sb.newLine () 157 | sb.ToString() 158 | 159 | let private printRequestAndResponse (response: Response) = 160 | let sb = StringBuilder() 161 | sb.newLine () 162 | sb.append (doPrintRequestOnly (response.version.ToString()) response.request response.requestMessage) 163 | sb.append (printResponseOnly response) 164 | 165 | sb.ToString() 166 | 167 | module Request = 168 | let print (request: IToRequest) = printRequestOnly request 169 | 170 | module Response = 171 | let print (response: Response) = printRequestAndResponse response 172 | -------------------------------------------------------------------------------- /src/FsHttp/Request.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Request 2 | 3 | open System 4 | open System.Net.Http 5 | open System.Net.Http.Headers 6 | 7 | open FsHttp 8 | open FsHttp.Helper 9 | 10 | // TODO: Remove this 11 | let getAddressDefaults (request: Request) = 12 | let uri = request.header.target.ToUriStringWithDefault("") 13 | let method = request.header.target.method |> Option.defaultValue HttpMethod.Get 14 | uri, method 15 | 16 | let addressToString (request: Request) = 17 | let uri, method = getAddressDefaults request 18 | $"{method} {uri}" 19 | 20 | /// Transforms a Request into a System.Net.Http.HttpRequestMessage. 21 | let toRequestAndMessage (request: IToRequest) : Request * HttpRequestMessage = 22 | let request = 23 | let mutable request = request.ToRequest() 24 | for headerTransformer in request.config.headerTransformers do 25 | request <- { request with header = headerTransformer request.header } 26 | request 27 | 28 | // TODO: Try to encode URL / HTTP method presence or absence on type level. 29 | let uri, method = getAddressDefaults request 30 | 31 | let requestMessage = new HttpRequestMessage(method, uri) 32 | 33 | let buildDotnetContent 34 | (part: ContentData) 35 | (contentType: ContentType option) 36 | (name: string option) 37 | (fileName: string option) 38 | = 39 | let addDispoHeaderIfNeeded (content: HttpContent) = 40 | match request.content with 41 | | Multi _ -> 42 | let contentDispoHeaderValue = ContentDispositionHeaderValue("form-data") 43 | 44 | match name with 45 | | Some v -> contentDispoHeaderValue.Name <- v 46 | | None -> () 47 | 48 | match fileName with 49 | | Some v -> contentDispoHeaderValue.FileName <- v 50 | | None -> () 51 | 52 | content.Headers.ContentDisposition <- contentDispoHeaderValue 53 | () 54 | | _ -> () 55 | 56 | let dotnetContent = 57 | match part with 58 | | TextContent s -> 59 | // TODO: Encoding is set hard to UTF8 - but the HTTP request has it's own encoding header. 60 | let content = new StringContent(s) :> HttpContent 61 | addDispoHeaderIfNeeded content 62 | content 63 | | BinaryContent data -> 64 | let content = new ByteArrayContent(data) :> HttpContent 65 | addDispoHeaderIfNeeded content 66 | content 67 | | StreamContent s -> 68 | let content = new StreamContent(s) :> HttpContent 69 | addDispoHeaderIfNeeded content 70 | content 71 | | FormUrlEncodedContent data -> new FormUrlEncodedContent(data) :> HttpContent 72 | | FileContent path -> 73 | let content = 74 | let fs = System.IO.File.OpenRead path 75 | new StreamContent(fs) 76 | 77 | addDispoHeaderIfNeeded content 78 | content 79 | 80 | match contentType with 81 | | Some contentType -> do dotnetContent.Headers.ContentType <- contentType.ToMediaHeaderValue() 82 | | _ -> () 83 | 84 | dotnetContent 85 | 86 | let assignContentHeaders (target: HttpHeaders) (headers: Map) = 87 | for kvp in headers do 88 | target.Add(kvp.Key, kvp.Value) 89 | 90 | let dotnetContent = 91 | match request.content with 92 | | Empty -> null 93 | | Single bodyContent -> 94 | let dotnetBodyContent = 95 | buildDotnetContent 96 | bodyContent.contentElement.contentData 97 | bodyContent.contentElement.explicitContentType 98 | None 99 | None 100 | 101 | do assignContentHeaders dotnetBodyContent.Headers bodyContent.headers 102 | dotnetBodyContent 103 | | Multi multipartContent -> 104 | let dotnetMultipartContent = 105 | match multipartContent.partElements with 106 | | [] -> null 107 | | parts -> 108 | let dotnetPart = new MultipartFormDataContent() 109 | 110 | for part in parts do 111 | let dotnetContent = 112 | buildDotnetContent 113 | part.content.contentData 114 | part.content.explicitContentType 115 | (Some part.name) 116 | part.fileName 117 | 118 | dotnetPart.Add(dotnetContent, part.name) 119 | 120 | dotnetPart :> HttpContent 121 | 122 | do assignContentHeaders dotnetMultipartContent.Headers multipartContent.headers 123 | dotnetMultipartContent 124 | 125 | do 126 | requestMessage.Content <- dotnetContent 127 | assignContentHeaders requestMessage.Headers request.header.headers 128 | 129 | request, requestMessage 130 | 131 | let toRequest request = request |> toRequestAndMessage |> fst 132 | let toHttpRequestMessage request = request |> toRequestAndMessage |> snd 133 | 134 | /// Builds an asynchronous request, without sending it. 135 | let toAsync cancellationTokenOverride (context: IToRequest) = 136 | async { 137 | let request, requestMessage = toRequestAndMessage context 138 | do Fsi.logfn $"Sending request {addressToString request} ..." 139 | 140 | use finalRequestMessage = 141 | request.config.httpMessageTransformers 142 | |> List.fold (fun c n -> n c) requestMessage 143 | 144 | // cancellationTokenOverride: Because of C# interop (see Extensions) 145 | let ctok = 146 | match cancellationTokenOverride with 147 | | Some ctok -> ctok 148 | | None -> request.config.cancellationToken 149 | let client = request.config.httpClientFactory request.config 150 | 151 | match request.header.cookies with 152 | | [] -> () 153 | | cookies -> 154 | let cookies = cookies |> List.map string |> String.concat "; " 155 | do finalRequestMessage.Headers.Add("Cookie", cookies) 156 | 157 | let finalClient = 158 | request.config.httpClientTransformers |> List.fold (fun c n -> n c) client 159 | 160 | let! response = 161 | finalClient.SendAsync(finalRequestMessage, request.config.httpCompletionOption, ctok) 162 | |> Async.AwaitTask 163 | 164 | if request.config.bufferResponseContent then 165 | // Task is started immediately, but must not be awaited when running in background. 166 | response.Content.LoadIntoBufferAsync() |> ignore 167 | 168 | do Fsi.logfn $"{response.StatusCode |> int} ({response.StatusCode}) ({addressToString request})" 169 | 170 | let dispose () = 171 | do finalClient.Dispose() 172 | do response.Dispose() 173 | do requestMessage.Dispose() 174 | 175 | return { 176 | request = request 177 | content = response.Content 178 | headers = response.Headers 179 | reasonPhrase = response.ReasonPhrase 180 | statusCode = response.StatusCode 181 | requestMessage = response.RequestMessage 182 | version = response.Version 183 | printHint = request.printHint 184 | originalHttpRequestMessage = requestMessage 185 | originalHttpResponseMessage = response 186 | dispose = dispose 187 | } 188 | } 189 | 190 | /// Sends a request asynchronously. 191 | let sendTAsync (request: IToRequest) = request |> toAsync None |> Async.StartAsTask 192 | 193 | /// Sends a request asynchronously. 194 | let sendAsync (request: IToRequest) = request |> sendTAsync |> Async.AwaitTask 195 | 196 | /// Sends a request synchronously. 197 | let send request = request |> toAsync None |> Async.RunSynchronously 198 | -------------------------------------------------------------------------------- /src/Test.CSharp/BasicTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | using FsHttp; 5 | using FsHttp.Tests; 6 | 7 | using NUnit.Framework; 8 | 9 | namespace Test.CSharp 10 | { 11 | public class BasicTests 12 | { 13 | [SetUp] 14 | public void Setup() { } 15 | 16 | [Test] 17 | public async Task PostsText() 18 | { 19 | const string Content = "Hello World"; 20 | 21 | using var server = Server.Predefined.postReturnsBody(); 22 | 23 | var response = await 24 | (await Server.url("").Post() 25 | .Body() 26 | .Text(Content) 27 | .SendAsync()) 28 | .ToTextAsync(); 29 | 30 | Assert.That(Content, Is.EqualTo(response)); 31 | } 32 | 33 | public record Person(string Name, string Job); 34 | 35 | [Test] 36 | public async Task PostsJsonObject() 37 | { 38 | var jsonObj = new Person("morpheus", "leader"); 39 | 40 | using var server = Server.Predefined.postReturnsBody(); 41 | 42 | var response = await 43 | (await Server.url("").Post() 44 | .Body() 45 | .JsonSerialize(jsonObj) 46 | .SendAsync()) 47 | .DeserializeJsonAsync(); 48 | 49 | Assert.That(jsonObj, Is.EqualTo(response)); 50 | } 51 | 52 | [Test] 53 | public void FluentConfig() 54 | { 55 | const string Content = "Hello World"; 56 | 57 | using var server = Server.Predefined.postReturnsBody(); 58 | 59 | Assert.ThrowsAsync(async () => 60 | await 61 | (await Server.url("").Post() 62 | .Body() 63 | .Text(Content) 64 | .Config().Timeout(TimeSpan.FromTicks(1)) 65 | .SendAsync()) 66 | .ToTextAsync()); 67 | } 68 | 69 | [Test, Ignore("Compiler-test only")] 70 | public void FluentPrintHint() 71 | { 72 | var request = 73 | Http.Get("http://...") 74 | .Print().HeaderOnly(); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/Test.CSharp/Test.CSharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/TestWebServer/Api/FileCallbackResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http.Headers; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Infrastructure; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace TestWebServer.Api 11 | { 12 | public class FileCallbackResult : FileResult 13 | { 14 | private readonly Func _callback; 15 | 16 | public FileCallbackResult(MediaTypeHeaderValue contentType, Func callback) 17 | : base(contentType?.ToString()) 18 | { 19 | _callback = callback; 20 | } 21 | 22 | public override Task ExecuteResultAsync(ActionContext context) 23 | { 24 | var executor = new FileCallbackResultExecutor( 25 | context.HttpContext.RequestServices.GetRequiredService()); 26 | return executor.ExecuteAsync(context, this); 27 | } 28 | 29 | private sealed class FileCallbackResultExecutor : FileResultExecutorBase 30 | { 31 | public FileCallbackResultExecutor(ILoggerFactory loggerFactory) 32 | : base(CreateLogger(loggerFactory)) 33 | { 34 | } 35 | 36 | public Task ExecuteAsync(ActionContext context, FileCallbackResult result) 37 | { 38 | SetHeadersAndLog(context, result, null, true); 39 | return result._callback(context.HttpContext.Response.Body, context); 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/TestWebServer/Api/TestController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Net.Http.Headers; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace TestWebServer.Api 10 | { 11 | [Route("test")] 12 | public class TestController : Controller 13 | { 14 | [HttpGet("lines")] 15 | public IActionResult Get() => 16 | new FileCallbackResult( 17 | new MediaTypeHeaderValue("application/octet-stream"), 18 | async (outputStream, _) => 19 | { 20 | await using var sw = new StreamWriter(outputStream); 21 | 22 | for (var i = 0; i < 1000; i++) 23 | { 24 | await sw.WriteLineAsync("skldfj skldfjklsdj flksdjfkl sdjlkfj sdlk fjskld fjlkj"); 25 | await Task.Delay(10); 26 | } 27 | }) 28 | { 29 | FileDownloadName = "MyZipfile.zip" 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/TestWebServer/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace TestWebServer 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/TestWebServer/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | 11 | namespace TestWebServer 12 | { 13 | public class Startup 14 | { 15 | // This method gets called by the runtime. Use this method to add services to the container. 16 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 17 | public void ConfigureServices(IServiceCollection services) 18 | { 19 | services.AddControllers(); 20 | } 21 | 22 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 23 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 24 | { 25 | if (env.IsDevelopment()) 26 | { 27 | app.UseDeveloperExceptionPage(); 28 | } 29 | 30 | app.UseRouting(); 31 | //app.UseEndpoints(endpoints => 32 | //{ 33 | // endpoints.MapGet("/", async context => 34 | // { 35 | // await context.Response.WriteAsync("Hello World!"); 36 | // }); 37 | //}); 38 | app.UseEndpoints(endpoints => 39 | { 40 | endpoints.MapControllers(); 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/TestWebServer/TestWebServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/TestWebServer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/TestWebServer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /src/Tests/AlternativeSyntaxes.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.``Alternative Syntaxes`` 2 | 3 | open FsUnit 4 | open FsHttp 5 | open FsHttp.Tests.Server 6 | 7 | open NUnit.Framework 8 | 9 | open Suave 10 | open Suave.Operators 11 | open Suave.Filters 12 | open Suave.Successful 13 | 14 | [] 15 | let ``Shortcut for GET works`` () = 16 | use server = GET >=> request (fun r -> r.rawQuery |> OK) |> serve 17 | 18 | get (url @"?test=Hallo") 19 | |> Request.send 20 | |> Response.toText 21 | |> shouldEqual "test=Hallo" 22 | -------------------------------------------------------------------------------- /src/Tests/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | module AssemblyInfo 2 | 3 | [] 4 | do () 5 | -------------------------------------------------------------------------------- /src/Tests/Basic.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Basic 2 | 3 | open System 4 | open System.Text 5 | open System.Threading 6 | 7 | open FsUnit 8 | open FsHttp 9 | open FsHttp.Tests.TestHelper 10 | open FsHttp.Tests.Server 11 | 12 | open NUnit.Framework 13 | 14 | open Suave 15 | open Suave.Operators 16 | open Suave.Filters 17 | open Suave.Successful 18 | 19 | 20 | [] 21 | let ``Synchronous calls are invoked immediately`` () = 22 | use server = GET >=> request (fun r -> r.rawQuery |> OK) |> serve 23 | 24 | get (url @"?test=Hallo") 25 | |> Request.send 26 | |> Response.toText 27 | |> should equal "test=Hallo" 28 | 29 | 30 | [] 31 | let ``Asynchronous calls are sent immediately`` () = 32 | 33 | let mutable time = DateTime.MaxValue 34 | 35 | use server = 36 | GET 37 | >=> request (fun r -> 38 | time <- DateTime.Now 39 | r.rawQuery |> OK 40 | ) 41 | |> serve 42 | 43 | let req = get (url "?test=Hallo") |> Request.sendAsync 44 | 45 | Thread.Sleep 3000 46 | 47 | req |> Async.RunSynchronously |> Response.toText |> should equal "test=Hallo" 48 | 49 | (DateTime.Now - time > TimeSpan.FromSeconds 2.0) |> should equal true 50 | 51 | 52 | [] 53 | let ``Split URL are interpreted correctly`` () = 54 | use server = GET >=> request (fun r -> r.rawQuery |> OK) |> serve 55 | 56 | http { 57 | GET( 58 | url 59 | @" 60 | ?test=Hallo 61 | &test2=Welt" 62 | ) 63 | } 64 | |> Request.send 65 | |> Response.toText 66 | |> should equal "test=Hallo&test2=Welt" 67 | 68 | 69 | [] 70 | let ``Smoke test for a header`` () = 71 | use server = GET >=> request (header "accept-language" >> OK) |> serve 72 | 73 | let lang = "zh-Hans" 74 | 75 | http { 76 | GET(url @"") 77 | AcceptLanguage lang 78 | } 79 | |> Request.send 80 | |> Response.toText 81 | |> should equal lang 82 | 83 | [] 84 | let ``Smoke test for headers`` () = 85 | let headersToString = 86 | List.sort 87 | >> List.map (fun (key, value) -> $"{key}={value}".ToLower()) 88 | >> (fun h -> String.Join("&", h)) 89 | 90 | let headerPrefix = "X-Custom-Value" 91 | let customHeaders = [ for i in 1..10 -> $"{headerPrefix}{i}", $"{i}" ] 92 | let expected = headersToString customHeaders 93 | 94 | use server = 95 | GET 96 | >=> request (fun r -> 97 | let headers = 98 | r.headers 99 | |> List.filter (fun (k, _) -> k.StartsWith(headerPrefix, StringComparison.OrdinalIgnoreCase)) 100 | 101 | headersToString headers |> OK 102 | ) 103 | |> serve 104 | 105 | http { 106 | GET(url @"") 107 | headers customHeaders 108 | } 109 | |> Request.send 110 | |> Response.toText 111 | |> should equal expected 112 | 113 | [] 114 | let ``ContentType override`` () = 115 | use server = POST >=> request (header "content-type" >> OK) |> serve 116 | 117 | let contentType = "text/xxx" 118 | 119 | http { 120 | POST(url @"") 121 | body 122 | ContentType contentType 123 | text "hello world" 124 | } 125 | |> Request.send 126 | |> Response.toText 127 | |> should contain contentType 128 | 129 | 130 | [] 131 | let ``ContentType with encoding`` () = 132 | use server = POST >=> request (header "content-type" >> OK) |> serve 133 | 134 | let contentType = "text/xxx" 135 | let expectedContentTypeHeader = $"{contentType}; charset=utf-8" 136 | 137 | http { 138 | POST(url @"") 139 | body 140 | ContentType contentType Encoding.UTF8 141 | text "hello world" 142 | } 143 | |> Request.send 144 | |> Response.toText 145 | |> should contain expectedContentTypeHeader 146 | -------------------------------------------------------------------------------- /src/Tests/Body.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Body 2 | 3 | open FsUnit 4 | open FsHttp 5 | open FsHttp.Tests.TestHelper 6 | open FsHttp.Tests.Server 7 | 8 | open NUnit.Framework 9 | 10 | open Suave 11 | open Suave.Operators 12 | open Suave.Filters 13 | open Suave.Successful 14 | 15 | 16 | [] 17 | let ``POST string data`` () = 18 | use server = POST >=> request (contentText >> OK) |> serve 19 | 20 | let data = "hello world" 21 | 22 | http { 23 | POST(url @"") 24 | body 25 | text data 26 | } 27 | |> Request.send 28 | |> Response.toText 29 | |> should equal data 30 | 31 | 32 | [] 33 | let ``POST binary data`` () = 34 | use server = POST >=> request (fun r -> r.rawForm |> Suave.Successful.ok) |> serve 35 | 36 | let data = [| 12uy; 22uy; 99uy |] 37 | 38 | http { 39 | POST(url @"") 40 | body 41 | binary data 42 | } 43 | |> Request.send 44 | |> Response.toBytes 45 | |> should equal data 46 | 47 | 48 | [] 49 | let ``POST Form url encoded data`` () = 50 | use server = 51 | POST >=> request (fun r -> (form "q1" r) + "_" + (form "q2" r) |> OK) |> serve 52 | 53 | http { 54 | POST(url @"") 55 | body 56 | formUrlEncoded [ "q1", "Query1"; "q2", "Query2" ] 57 | } 58 | |> Request.send 59 | |> Response.toText 60 | |> should equal ("Query1_Query2") 61 | 62 | 63 | [] 64 | let ``Specify content type explicitly`` () = 65 | use server = POST >=> request (header "content-type" >> OK) |> serve 66 | 67 | let contentType = "text/whatever" 68 | 69 | http { 70 | POST(url @"") 71 | body 72 | ContentType contentType 73 | } 74 | |> Request.send 75 | |> Response.toText 76 | |> should equal contentType 77 | 78 | 79 | [] 80 | let ``Default content type for JSON is specified correctly`` () = 81 | use server = POST >=> request (header "content-type" >> OK) |> serve 82 | 83 | http { 84 | POST(url @"") 85 | body 86 | json " [] " 87 | } 88 | |> Request.send 89 | |> Response.toText 90 | |> should equal MimeTypes.applicationJson 91 | 92 | 93 | [] 94 | let ``Explicitly specified content type is dominant`` () = 95 | use server = POST >=> request (header "content-type" >> OK) |> serve 96 | 97 | let explicitContentType = "text/whatever" 98 | 99 | http { 100 | POST(url @"") 101 | body 102 | ContentType explicitContentType 103 | json " [] " 104 | } 105 | |> Request.send 106 | |> Response.toText 107 | |> should equal explicitContentType 108 | 109 | 110 | [] 111 | let ``Content length automatically set`` () = 112 | use server = POST >=> request (header "content-length" >> OK) |> serve 113 | 114 | let contentData = " [] " 115 | 116 | http { 117 | POST(url @"") 118 | body 119 | json contentData 120 | } 121 | |> Request.send 122 | |> Response.toText 123 | |> should equal (contentData.Length.ToString()) 124 | 125 | // TODO: Post single file 126 | // TODO: POST stream 127 | -------------------------------------------------------------------------------- /src/Tests/BuildersAndSignatures.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.``Builders and Signatures`` 2 | 3 | open System 4 | open System.Net.Http 5 | open FsHttp 6 | open NUnit.Framework 7 | open FsUnit 8 | 9 | let signatures () = 10 | let _: IToRequest = http { GET "" } 11 | let _: Request = http { GET "" } |> Request.toRequest 12 | let _: HttpRequestMessage = http { GET "" } |> Request.toHttpRequestMessage 13 | let _: Async = http { GET "" } |> Request.toAsync None 14 | let _: Async = http { GET "" } |> Request.sendAsync 15 | let _: Response = http { GET "" } |> Request.send 16 | () 17 | 18 | let ``Shortcuts work - had problems with resolution before`` () = 19 | get "https://myService" { 20 | multipart 21 | textPart "" "" 22 | } 23 | 24 | (* 25 | let ``Explicit 'body' keyword is needed for describing request body`` () = 26 | http { 27 | GET "" 28 | json "" 29 | } 30 | *) 31 | 32 | (* 33 | let ``Explicit 'multibody' keyword is needed for describing request body`` () = 34 | http { 35 | GET "" 36 | textPart "" 37 | } 38 | *) 39 | 40 | let ``General configuration is possible on all builder contextx`` () = 41 | http { 42 | config_timeoutInSeconds 1.0 43 | GET "http://myService.com" 44 | } 45 | |> ignore 46 | 47 | http { 48 | GET "http://myService.com" 49 | config_timeoutInSeconds 1.0 50 | } 51 | |> ignore 52 | 53 | http { 54 | GET "http://myService.com" 55 | body 56 | text "" 57 | config_timeoutInSeconds 1.0 58 | } 59 | |> ignore 60 | 61 | 62 | http { 63 | GET "http://myService.com" 64 | multipart 65 | textPart "" "" 66 | config_timeoutInSeconds 1.0 67 | } 68 | |> ignore 69 | 70 | let ``Print configuration is possible on all builder contextx`` () = 71 | http { 72 | print_headerOnly 73 | GET "http://myService.com" 74 | } 75 | |> ignore 76 | 77 | http { 78 | GET "http://myService.com" 79 | print_headerOnly 80 | } 81 | |> ignore 82 | 83 | http { 84 | GET "http://myService.com" 85 | body 86 | text "" 87 | print_headerOnly 88 | } 89 | |> ignore 90 | 91 | http { 92 | GET "http://myService.com" 93 | multipart 94 | textPart "" "" 95 | print_headerOnly 96 | } 97 | |> ignore 98 | 99 | [] 100 | let ``Config of StartingContext is taken`` () = 101 | let timeout = TimeSpan.FromSeconds 22.2 102 | 103 | let req = 104 | http { 105 | config_timeout timeout 106 | GET "http://myservice" 107 | } 108 | 109 | GlobalConfig.defaults.Config.timeout |> should not' (equal (Some timeout)) 110 | req.config.timeout |> should equal (Some timeout) 111 | -------------------------------------------------------------------------------- /src/Tests/Config.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Config 2 | 3 | open System 4 | open System.Text 5 | open System.Threading 6 | open System.Threading.Tasks 7 | 8 | open FsUnit 9 | open FsHttp 10 | open FsHttp.Tests.TestHelper 11 | open FsHttp.Tests.Server 12 | 13 | open NUnit.Framework 14 | 15 | open Suave 16 | open Suave.Operators 17 | open Suave.Filters 18 | open Suave.Successful 19 | 20 | open NUnit.Framework 21 | open System.Net.Http 22 | 23 | let timeoutEquals (config: Config) totalSeconds = 24 | config.timeout 25 | |> Option.map (fun x -> x.TotalSeconds) 26 | |> should equal (Some totalSeconds) 27 | 28 | [] 29 | let ``Global config snapshot is used in moment of request creation`` () = 30 | 31 | let setTimeout t = GlobalConfig.defaults |> Config.timeoutInSeconds t |> GlobalConfig.set 32 | 33 | let t1 = 11.5 34 | let t2 = 22.5 35 | 36 | do GlobalConfig.defaults.Config.timeout |> should not' (equal (Some t1)) 37 | do GlobalConfig.defaults.Config.timeout |> should not' (equal (Some t2)) 38 | 39 | setTimeout t1 40 | 41 | let r1 = http { GET(url @"") } 42 | 43 | setTimeout t2 44 | 45 | // still t1 46 | do timeoutEquals r1.config t1 47 | 48 | let r2 = http { GET(url @"") } 49 | 50 | do timeoutEquals r2.config t2 51 | 52 | let private serverWithRequestDuration (requestTime: TimeSpan) = 53 | GET 54 | >=> request (fun r -> 55 | Thread.Sleep requestTime 56 | "" |> OK 57 | ) 58 | |> serve 59 | 60 | 61 | let sendRequestWithTimeout timeout = 62 | fun () -> 63 | let req = get (url "") 64 | 65 | let req = 66 | match timeout with 67 | | Some timeout -> req { config_timeoutInSeconds timeout } 68 | | None -> req 69 | 70 | req { 71 | config_transformHttpClient (fun (client: HttpClient) -> 72 | printfn "TIMEOUT: %A" client.Timeout 73 | client 74 | ) 75 | } 76 | |> Request.send 77 | |> ignore 78 | 79 | 80 | [] 81 | let ``Timeout config per request - success expected`` () = 82 | use server = serverWithRequestDuration (TimeSpan.FromSeconds 1.0) 83 | 84 | (sendRequestWithTimeout (Some 20.0)) () 85 | 86 | 87 | [] 88 | let ``Timeout config per request - timeout expected`` () = 89 | use server = serverWithRequestDuration (TimeSpan.FromSeconds 10.0) 90 | sendRequestWithTimeout (Some 1.0) |> should throw typeof 91 | 92 | 93 | [] 94 | let ``Timeout config global - success expected`` () = 95 | use server = serverWithRequestDuration (TimeSpan.FromSeconds 1.0) 96 | 97 | GlobalConfig.defaults |> Config.timeoutInSeconds 20.0 |> GlobalConfig.set 98 | 99 | (sendRequestWithTimeout None) () 100 | 101 | 102 | [] 103 | let ``Timeout config global - timeout expected`` () = 104 | use server = serverWithRequestDuration (TimeSpan.FromSeconds 10.0) 105 | 106 | GlobalConfig.defaults |> Config.timeoutInSeconds 20.0 |> GlobalConfig.set 107 | 108 | (sendRequestWithTimeout None) () 109 | 110 | GlobalConfig.defaults |> Config.timeoutInSeconds 1.0 |> GlobalConfig.set 111 | 112 | sendRequestWithTimeout None |> should throw typeof 113 | 114 | 115 | [] 116 | let ``Cancellation token can be supplied by user`` () = 117 | let serverRequestDuration = TimeSpan.FromSeconds 10.0 118 | let clientRequestDuration = TimeSpan.FromSeconds 3.0 119 | let expectedOverheadTime = TimeSpan.FromSeconds 2.0 120 | 121 | use server = serverWithRequestDuration serverRequestDuration 122 | 123 | (sendRequestWithTimeout None) () 124 | 125 | use cs = new CancellationTokenSource() 126 | 127 | Thread(fun () -> 128 | Thread.Sleep clientRequestDuration 129 | cs.Cancel() 130 | ) 131 | .Start() 132 | 133 | let requestStartTime = DateTime.Now 134 | 135 | let mutable wasCancelled = false 136 | 137 | try 138 | get (url "") { config_cancellationToken cs.Token } |> Request.send |> ignore 139 | with :? TaskCanceledException -> 140 | wasCancelled <- true 141 | 142 | let requestDuration = DateTime.Now - requestStartTime 143 | 144 | (requestDuration + expectedOverheadTime < serverRequestDuration) 145 | |> should equal true 146 | 147 | wasCancelled |> should equal true 148 | 149 | 150 | let [] ``Pre-Configured Requests``() = 151 | let headerName = "X-Custom-Value" 152 | let headerValue = "Hallo Welt" 153 | 154 | use server = GET >=> request (header headerName >> OK) |> serve 155 | 156 | let httpSpecial = 157 | http { 158 | header headerName headerValue 159 | } 160 | 161 | let response = 162 | httpSpecial { 163 | GET (url @"") 164 | } 165 | |> Request.send 166 | |> Response.toText 167 | 168 | do printfn "RESPONSE: %A" response 169 | 170 | response |> should equal headerValue 171 | 172 | 173 | let [] ``Header Transformer``() = 174 | let url = "http://" 175 | let urlSuffix1 = "suffix1" 176 | let urlSuffix2 = "suffix2" 177 | 178 | let httpSpecial = 179 | let transformWith suffix = 180 | fun (header: Header) -> 181 | let address = (header.target.address |> Option.defaultValue "") 182 | { header with target.address = Some $"{address}{suffix}" } 183 | http { 184 | config_transformHeader (transformWith urlSuffix1) 185 | config_transformHeader (transformWith urlSuffix2) 186 | } 187 | 188 | httpSpecial { 189 | GET url 190 | } 191 | |> Request.toRequestAndMessage 192 | |> fst 193 | |> _.header.target.address 194 | |> Option.defaultValue "" 195 | |> should equal $"{url}{urlSuffix1}{urlSuffix2}" 196 | -------------------------------------------------------------------------------- /src/Tests/Cookies.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Cookies 2 | 3 | open FsUnit 4 | open FsHttp 5 | open FsHttp.Tests.Server 6 | 7 | open NUnit.Framework 8 | 9 | open Suave 10 | open Suave.Cookie 11 | open Suave.Operators 12 | open Suave.Filters 13 | open Suave.Successful 14 | 15 | 16 | [] 17 | let ``Cookies can be sent`` () = 18 | use server = 19 | GET 20 | >=> request (fun r -> r.cookies |> Map.find "test" |> (fun httpCookie -> httpCookie.value) |> OK) 21 | |> serve 22 | 23 | http { 24 | GET(url @"") 25 | Cookie "test" "hello world" 26 | } 27 | |> Request.send 28 | |> Response.toText 29 | |> should equal "hello world" 30 | -------------------------------------------------------------------------------- /src/Tests/DotNetHttp.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.DotNetHttp 2 | 3 | open System.Net.Http 4 | open System.Threading 5 | 6 | open FsUnit 7 | open FsHttp 8 | open FsHttp.Tests.Server 9 | 10 | open NUnit.Framework 11 | 12 | open Suave.Operators 13 | open Suave.Filters 14 | open Suave.Successful 15 | 16 | 17 | [] 18 | let ``Inject custom HttpClient factory`` () = 19 | let executedFlag = "executed" 20 | 21 | use server = GET >=> OK executedFlag |> serve 22 | 23 | let mutable intercepted = false 24 | 25 | let interceptor = 26 | { new DelegatingHandler(InnerHandler = new HttpClientHandler()) with 27 | member _.SendAsync(request: HttpRequestMessage, cancellationToken: CancellationToken) = 28 | intercepted <- true 29 | base.SendAsync(request, cancellationToken) 30 | } 31 | 32 | intercepted |> should equal false 33 | 34 | http { 35 | config_setHttpClientFactory (fun _ -> new HttpClient(interceptor)) 36 | GET(url "") 37 | } 38 | |> Request.send 39 | |> Response.toText 40 | |> should equal executedFlag 41 | 42 | intercepted |> should equal true 43 | -------------------------------------------------------------------------------- /src/Tests/Expectations.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Expectations 2 | 3 | open NUnit.Framework 4 | 5 | open FsHttp 6 | open FsHttp.Tests.Server 7 | 8 | open Suave.ServerErrors 9 | open Suave.Operators 10 | open Suave.Filters 11 | 12 | // TODO: exactMatch = true 13 | 14 | [] 15 | let ``Expect status code`` () = 16 | use server = GET >=> BAD_GATEWAY "" |> serve 17 | 18 | http { GET(url @"") } 19 | |> Request.send 20 | |> Response.assertHttpStatusCode System.Net.HttpStatusCode.BadGateway 21 | |> ignore 22 | 23 | Assert.Throws(fun () -> 24 | http { GET(url @"") } 25 | |> Request.send 26 | |> Response.assertHttpStatusCode System.Net.HttpStatusCode.Ambiguous 27 | |> ignore 28 | ) 29 | |> ignore 30 | -------------------------------------------------------------------------------- /src/Tests/ExtendingBuilders.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.``Extending Builders`` 2 | 3 | open FsUnit 4 | open FsHttp 5 | open FsHttp.Tests.TestHelper 6 | open FsHttp.Tests.Server 7 | 8 | open NUnit.Framework 9 | 10 | open Suave 11 | open Suave.Operators 12 | open Suave.Filters 13 | open Suave.Successful 14 | 15 | let superBodyContentType = { 16 | ContentType.value = "text/superCsv" 17 | charset = Some System.Text.Encoding.UTF32 18 | } 19 | 20 | type IRequestContext<'self> with 21 | [] 22 | member this.SuperBody(context: IRequestContext, csvContent: string) = 23 | FsHttp.Dsl.Body.content superBodyContentType (TextContent csvContent) context.Self 24 | 25 | 26 | [] 27 | let ``Extending builder with custom content`` () = 28 | 29 | let dummyContent = "Hello;World" 30 | 31 | use server = 32 | POST 33 | >=> request ( 34 | fun r -> 35 | let header = header "content-type" r 36 | let content = contentText r 37 | $"{header} - {content}" 38 | >> OK 39 | ) 40 | |> serve 41 | 42 | http { 43 | POST(url @"") 44 | body 45 | superBody dummyContent 46 | } 47 | |> Request.send 48 | |> Response.toText 49 | |> should equal $"{superBodyContentType.value}; charset=utf-32 - {dummyContent}" 50 | -------------------------------------------------------------------------------- /src/Tests/Helper.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Helper 2 | 3 | open System 4 | open System.IO 5 | open System.Text 6 | 7 | open FsUnit 8 | open FsHttp.Helper 9 | open FsHttp.Tests 10 | 11 | open NUnit.Framework 12 | 13 | [] 14 | let ``URL combine`` () = 15 | let a = "http://xxx.com" 16 | let b = "sub" 17 | let expectedUrl = $"{a}/{b}" 18 | 19 | Url.combine $"{a}" $"{b}" |> should equal expectedUrl 20 | Url.combine $"{a}/" $"{b}" |> should equal expectedUrl 21 | Url.combine $"{a}" $"/{b}" |> should equal expectedUrl 22 | Url.combine $"{a}/" $"/{b}" |> should equal expectedUrl 23 | Url.combine $"{a}/" $"/{b}/" |> should equal expectedUrl 24 | 25 | [] 26 | let ``Stream ReadUtf8StringAsync`` () = 27 | 28 | let text = "a😉b🙁🙂d" 29 | 30 | let test len (expected: string) = 31 | let res = 32 | new MemoryStream(Encoding.UTF8.GetBytes(text)) 33 | |> Stream.readUtf8StringAsync len 34 | |> Async.RunSynchronously 35 | let s1 = Encoding.UTF8.GetBytes res |> Array.toList 36 | let s2 = Encoding.UTF8.GetBytes expected |> Array.toList 37 | let res = (s1 = s2) 38 | if not res then 39 | printfn "" 40 | printfn "count = %d" len 41 | printfn "expected = %s" expected 42 | printfn "" 43 | printfn "Expected: %A" s2 44 | printfn "" 45 | printfn "Actual : %A" s1 46 | printfn "" 47 | printfn " ----------------------------" 48 | res |> should equal true 49 | 50 | test 0 "" 51 | test 1 "a" 52 | test 2 "a😉" 53 | test 3 "a😉b" 54 | test 4 "a😉b🙁" 55 | test 5 "a😉b🙁🙂" 56 | test 6 "a😉b🙁🙂d" 57 | test 100 "a😉b🙁🙂d" 58 | 59 | // TODO: Test other helper functions 60 | -------------------------------------------------------------------------------- /src/Tests/Helper/Server.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Server 2 | 3 | open System.Threading 4 | open Suave 5 | 6 | type Route = { 7 | method: WebPart 8 | route: string 9 | handler: HttpRequest -> WebPart 10 | } 11 | 12 | let url (s: string) = $"http://127.0.0.1:8080{s}" 13 | 14 | let serve (app: WebPart) = 15 | let cts = new CancellationTokenSource() 16 | let conf = { defaultConfig with cancellationToken = cts.Token } 17 | let listening, server = startWebServerAsync conf app 18 | 19 | Async.Start(server, cts.Token) 20 | 21 | do 22 | listening 23 | |> Async.RunSynchronously 24 | |> Array.choose id 25 | |> Array.map (fun x -> x.binding |> string) 26 | |> String.concat "; " 27 | |> printfn "Server ready and listening on: %s" 28 | 29 | let dispose () = 30 | cts.Cancel() 31 | cts.Dispose() 32 | 33 | { new System.IDisposable with 34 | member this.Dispose() = dispose () 35 | } 36 | 37 | module Predefined = 38 | open Suave 39 | open Suave.Operators 40 | open Suave.Filters 41 | open Suave.Successful 42 | 43 | open FsHttp.Tests.TestHelper 44 | 45 | let postReturnsBody () = POST >=> request (contentText >> OK) |> serve 46 | -------------------------------------------------------------------------------- /src/Tests/Helper/TestHelper.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FsHttp.Tests.TestHelper 3 | 4 | open System.Text 5 | 6 | open Suave 7 | open Suave.Utils.Collections 8 | open FsUnit 9 | 10 | let assertionExn (msg: string) = 11 | let otype = 12 | [ 13 | "Xunit.Sdk.XunitException, xunit.assert" 14 | "NUnit.Framework.AssertionException, nunit.framework" 15 | "Expecto.AssertException, expecto" 16 | ] 17 | |> List.tryPick (System.Type.GetType >> Option.ofObj) 18 | 19 | match otype with 20 | | None -> failwith msg 21 | | Some t -> 22 | let ctor = t.GetConstructor [| typeof |] 23 | ctor.Invoke [| msg |] :?> exn 24 | 25 | let joinLines lines = String.concat "\n" lines 26 | let keyNotFoundString = "KEY_NOT_FOUND" 27 | let query key (r: HttpRequest) = defaultArg (Option.ofChoice (r.query ^^ key)) keyNotFoundString 28 | let header key (r: HttpRequest) = defaultArg (Option.ofChoice (r.header key)) keyNotFoundString 29 | let form key (r: HttpRequest) = defaultArg (Option.ofChoice (r.form ^^ key)) keyNotFoundString 30 | let contentText (r: HttpRequest) = r.rawForm |> Encoding.UTF8.GetString 31 | 32 | let shouldEqual (a: 'a) (b: 'b) = a |> should equal b 33 | let shouldNotEquals (a: 'a) (b: 'b) = a |> should not' (equal b) 34 | -------------------------------------------------------------------------------- /src/Tests/Json.FSharpData.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Json.FSharpData 2 | 3 | open System 4 | open FSharp.Data 5 | open FsHttp.FSharpData 6 | open FsUnit 7 | open NUnit.Framework 8 | 9 | 10 | [] 11 | let ``To JSON`` () = 12 | 13 | let referenceJson = """ { "a": "aValue", "b": 12 } """ 14 | let expectedJson = """ { "a": "aValue" } """ 15 | 16 | referenceJson |> JsonValue.Parse |> Json.expectJsonSubset expectedJson |> ignore 17 | 18 | (fun () -> referenceJson |> JsonValue.Parse |> Json.expectJsonExact expectedJson |> ignore) 19 | |> should throw typeof 20 | -------------------------------------------------------------------------------- /src/Tests/Json.NewtonsoftJson.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Json.NewtonsoftJson 2 | 3 | open FsUnit 4 | open FsHttp 5 | open FsHttp.Tests.TestHelper 6 | open FsHttp.Tests.Server 7 | open FsHttp.NewtonsoftJson 8 | 9 | open Newtonsoft.Json.Linq 10 | open NUnit.Framework 11 | 12 | open Suave 13 | open Suave.Operators 14 | open Suave.Filters 15 | open Suave.Successful 16 | 17 | type Person = { 18 | name: string 19 | age: int 20 | } 21 | 22 | let returnBody () = POST >=> request (contentText >> OK) |> serve 23 | 24 | [] 25 | let ``Serialize / Deserialize JSON object`` () = 26 | use server = returnBody () 27 | 28 | let person = { 29 | name = "John Doe" 30 | age = 34 31 | } 32 | 33 | http { 34 | POST(url "") 35 | body 36 | jsonSerialize person 37 | } 38 | |> Request.send 39 | |> Response.deserializeJson 40 | |> shouldEqual person 41 | 42 | [] 43 | let ``To JSON and dynamic operator`` () = 44 | use server = returnBody () 45 | 46 | let jsonString = 47 | """ 48 | { 49 | "name": "John Doe", 50 | "age": 34 51 | } 52 | """ 53 | 54 | let json = 55 | http { 56 | POST(url "") 57 | body 58 | json jsonString 59 | } 60 | |> Request.send 61 | |> Response.toJson 62 | 63 | json?name.ToObject() |> should equal "John Doe" 64 | json?age.ToObject() |> should equal 34 65 | 66 | [] 67 | let ``To JSON array`` () = 68 | use server = returnBody () 69 | 70 | let jsonString = 71 | """ 72 | [ 73 | { 74 | "name": "John Doe", 75 | "age": 34 76 | }, 77 | { 78 | "name": "Foo Bar", 79 | "age": 99 80 | } 81 | ] 82 | """ 83 | 84 | http { 85 | POST(url "") 86 | body 87 | json jsonString 88 | } 89 | |> Request.send 90 | |> Response.toJsonSeq 91 | |> Seq.map (fun json -> json?name.ToObject()) 92 | |> Seq.toList 93 | |> shouldEqual [ "John Doe"; "Foo Bar" ] 94 | 95 | [] 96 | let ``Unicode chars`` () = 97 | use server = returnBody () 98 | 99 | let name = "John+Doe" 100 | 101 | http { 102 | POST(url "") 103 | body 104 | 105 | json ( 106 | sprintf 107 | """ 108 | { 109 | "name": "%s" 110 | } 111 | """ 112 | name 113 | ) 114 | } 115 | |> Request.send 116 | |> Response.toJson 117 | |> fun json -> json?name.ToObject() 118 | |> Seq.toList 119 | |> shouldEqual name 120 | -------------------------------------------------------------------------------- /src/Tests/Json.SystemText.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Json.SystemText 2 | 3 | open FsUnit 4 | open FsHttp 5 | open FsHttp.Tests.TestHelper 6 | open FsHttp.Tests.Server 7 | 8 | open NUnit.Framework 9 | 10 | open Suave 11 | open Suave.Operators 12 | open Suave.Filters 13 | open Suave.Successful 14 | 15 | open System.Text.Json 16 | open System.Text.Json.Serialization 17 | 18 | let returnBody () = POST >=> request (contentText >> OK) |> serve 19 | 20 | type Person = { 21 | name: string 22 | age: int 23 | } 24 | 25 | type SuperPerson = { 26 | name: string 27 | age: int 28 | address: string option 29 | } 30 | 31 | [] 32 | let ``Serialize / Deserialize JSON object`` () = 33 | use server = returnBody () 34 | 35 | let person = { 36 | name = "John Doe" 37 | age = 34 38 | } 39 | 40 | http { 41 | POST(url "") 42 | body 43 | jsonSerialize person 44 | } 45 | |> Request.send 46 | |> Response.deserializeJson 47 | |> shouldEqual person 48 | 49 | [] 50 | let ``Serialize / Deserialize JSON object with Tarmil`` () = 51 | use server = returnBody () 52 | 53 | FsHttp.GlobalConfig.Json.defaultJsonSerializerOptions <- 54 | let options = JsonSerializerOptions() 55 | options.Converters.Add(JsonFSharpConverter()) 56 | options 57 | 58 | let person1 = { 59 | name = "John Doe" 60 | age = 34 61 | address = Some "Whereever" 62 | } 63 | 64 | let person2 = { 65 | name = "Bryan Adams" 66 | age = 55 67 | address = None 68 | } 69 | 70 | let payload = [ person1; person2 ] 71 | 72 | http { 73 | POST(url "") 74 | body 75 | jsonSerialize payload 76 | } 77 | |> Request.send 78 | |> Response.deserializeJson 79 | |> shouldEqual payload 80 | 81 | [] 82 | let ``To JSON and dynamic operator`` () = 83 | use server = returnBody () 84 | 85 | let jsonString = 86 | """ 87 | { 88 | "name": "John Doe", 89 | "age": 34 90 | } 91 | """ 92 | 93 | let json = 94 | http { 95 | POST(url "") 96 | body 97 | json jsonString 98 | } 99 | |> Request.send 100 | |> Response.toJson 101 | 102 | json?name.GetString() |> should equal "John Doe" 103 | json?age.GetInt32() |> should equal 34 104 | 105 | [] 106 | let ``To JSON array`` () = 107 | use server = returnBody () 108 | 109 | let jsonString = 110 | """ 111 | [ 112 | { 113 | "name": "John Doe", 114 | "age": 34 115 | }, 116 | { 117 | "name": "Foo Bar", 118 | "age": 99 119 | } 120 | ] 121 | """ 122 | 123 | http { 124 | POST(url "") 125 | body 126 | json jsonString 127 | } 128 | |> Request.send 129 | |> Response.toJsonSeq 130 | |> Seq.map (fun json -> json?name.GetString()) 131 | |> Seq.toList 132 | |> shouldEqual [ "John Doe"; "Foo Bar" ] 133 | 134 | [] 135 | let ``Unicode chars`` () = 136 | use server = returnBody () 137 | 138 | let name = "John+Doe" 139 | 140 | http { 141 | POST(url "") 142 | body 143 | 144 | json ( 145 | sprintf 146 | """ 147 | { 148 | "name": "%s" 149 | } 150 | """ 151 | name 152 | ) 153 | } 154 | |> Request.send 155 | |> Response.toJson 156 | |> fun json -> json?name.GetString() 157 | |> Seq.toList 158 | |> shouldEqual name 159 | -------------------------------------------------------------------------------- /src/Tests/JsonComparison.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.``Json Comparison`` 2 | 3 | open System 4 | open FSharp.Data 5 | open FsHttp.FSharpData 6 | open FsUnit 7 | open NUnit.Framework 8 | 9 | 10 | [] 11 | let ``Simple property as subset`` () = 12 | 13 | let referenceJson = """ { "a": "aValue", "b": 12 } """ 14 | let expectedJson = """ { "a": "aValue" } """ 15 | 16 | referenceJson |> JsonValue.Parse |> Json.expectJsonSubset expectedJson |> ignore 17 | 18 | (fun () -> referenceJson |> JsonValue.Parse |> Json.expectJsonExact expectedJson |> ignore) 19 | |> should throw typeof 20 | 21 | [] 22 | let ``Property names are case sensitive`` () = 23 | 24 | let referenceJson = """ { "a": "aValue", "b": 12 } """ 25 | let expectedJson = """ { "A": "aValue" } """ 26 | 27 | (fun () -> referenceJson |> JsonValue.Parse |> Json.assertJsonSubset expectedJson |> ignore) 28 | |> should throw typeof 29 | 30 | (fun () -> referenceJson |> JsonValue.Parse |> Json.assertJsonExact expectedJson |> ignore) 31 | |> should throw typeof 32 | 33 | [] 34 | let ``Property values are case sensitive`` () = 35 | 36 | let referenceJson = """ { "a": "aValue", "b": 12 } """ 37 | let expectedJson = """ { "a": "AValue" } """ 38 | 39 | (fun () -> referenceJson |> JsonValue.Parse |> Json.assertJsonSubset expectedJson |> ignore) 40 | |> should throw typeof 41 | 42 | (fun () -> referenceJson |> JsonValue.Parse |> Json.assertJsonExact expectedJson |> ignore) 43 | |> should throw typeof 44 | 45 | [] 46 | let ``Arrays`` () = 47 | 48 | let referenceJson = """ [ 1, 2, 3, 4, 5 ] """ 49 | 50 | referenceJson 51 | |> JsonValue.Parse 52 | |> Json.assertJsonSubset """ [ 2, 3, 1 ] """ 53 | |> ignore 54 | 55 | (fun () -> 56 | referenceJson 57 | |> JsonValue.Parse 58 | |> Json.assertJson RespectOrder Subset """ [ 2, 3, 1 ] """ 59 | |> ignore 60 | ) 61 | |> should throw typeof 62 | 63 | referenceJson 64 | |> JsonValue.Parse 65 | |> Json.assertJson RespectOrder Subset """ [ 1, 2, 3 ] """ 66 | |> ignore 67 | 68 | (fun () -> 69 | referenceJson 70 | |> JsonValue.Parse 71 | |> Json.assertJsonExact """ [ 2, 3, 1 ] """ 72 | |> ignore 73 | ) 74 | |> should throw typeof 75 | 76 | (fun () -> 77 | referenceJson 78 | |> JsonValue.Parse 79 | |> Json.assertJson RespectOrder Exact """ [ 2, 3, 1, 5, 4 ] """ 80 | |> ignore 81 | ) 82 | |> should throw typeof 83 | 84 | referenceJson 85 | |> JsonValue.Parse 86 | |> Json.assertJson RespectOrder Exact """ [ 1, 2, 3, 4, 5 ] """ 87 | |> ignore 88 | 89 | 90 | [] 91 | let ``Exact Match Simple`` () = 92 | 93 | """ { "a": 1, "b": 2 } """ 94 | |> JsonValue.Parse 95 | |> Json.assertJsonExact """ { "a": 1, "b": 2 } """ 96 | |> ignore 97 | 98 | (fun () -> 99 | """ { "a": 1, "b": 2, "c": 3 } """ 100 | |> JsonValue.Parse 101 | |> Json.assertJsonExact """ { "a": 1, "b": 2 } """ 102 | |> ignore 103 | ) 104 | |> should throw typeof 105 | 106 | (fun () -> 107 | """ { "a": 1, "b": 2 } """ 108 | |> JsonValue.Parse 109 | |> Json.assertJsonExact """ { "a": 1, "b": 2, "c": 3 } """ 110 | |> ignore 111 | ) 112 | |> should throw typeof 113 | 114 | """ { "a": 1, "b": { "ba": 3, "bb": 4} } """ 115 | |> JsonValue.Parse 116 | |> Json.assertJsonExact """ { "a": 1, "b": { "ba": 3, "bb": 4} } """ 117 | |> ignore 118 | 119 | """ { "a": 1, "b": { "ba": 3, "bb": 4} } """ 120 | |> JsonValue.Parse 121 | |> Json.assertJsonExact """ { "a": 1, "b": { "ba": 3, "bb": 4} } """ 122 | |> ignore 123 | 124 | [] 125 | let ``Exact Match Complex`` () = 126 | 127 | """ { "a": 1, "b": { "ba": 3, "bb": 4 } } """ 128 | |> JsonValue.Parse 129 | |> Json.assertJsonExact """ { "a": 1, "b": { "ba": 3, "bb": 4 } } """ 130 | |> ignore 131 | 132 | (fun () -> 133 | """ { "a": 1, "b": { "ba": 3, "bb": 4, "bc": 5 } } """ 134 | |> JsonValue.Parse 135 | |> Json.assertJsonExact """ { "a": 1, "b": { "ba": 3, "bb": 4 } } """ 136 | |> ignore 137 | ) 138 | |> should throw typeof 139 | 140 | (fun () -> 141 | """ { "a": 1, "b": { "ba": 3, "bb": 4 } } """ 142 | |> JsonValue.Parse 143 | |> Json.assertJsonExact """ { "a": 1, "b": { "ba": 3, "bb": 4, "bc": 5 } } """ 144 | |> ignore 145 | ) 146 | |> should throw typeof 147 | -------------------------------------------------------------------------------- /src/Tests/Misc.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Misc 2 | 3 | open System 4 | open System.IO 5 | open FsUnit 6 | open FsHttp 7 | open FsHttp.Tests 8 | open FsHttp.Tests.Server 9 | 10 | open NUnit.Framework 11 | 12 | open Suave 13 | open Suave.Operators 14 | open Suave.Filters 15 | open Suave.Successful 16 | open Suave.Response 17 | open Suave.Writers 18 | 19 | 20 | [] 21 | let ``Custom HTTP method`` () = 22 | use server = 23 | ``method`` (HttpMethod.parse "FLY") >=> request (fun r -> OK "flying") |> serve 24 | 25 | http { Method "FLY" (url @"") } 26 | |> Request.send 27 | |> Response.toText 28 | |> should equal "flying" 29 | 30 | 31 | [] 32 | let ``Custom Header`` () = 33 | let customHeaderKey = "X-Custom-Value" 34 | 35 | use server = 36 | GET 37 | >=> request (fun r -> 38 | r.header customHeaderKey 39 | |> function 40 | | Choice1Of2 v -> v 41 | | Choice2Of2 e -> raise (assertionExn $"Failed {e}") 42 | |> OK 43 | ) 44 | |> serve 45 | 46 | http { 47 | GET(url @"") 48 | header customHeaderKey "hello world" 49 | } 50 | |> Request.send 51 | |> Response.toText 52 | |> should equal "hello world" 53 | 54 | [] 55 | let ``Custom Headers`` () = 56 | let headersToString = 57 | List.sort 58 | >> List.map (fun (key, value) -> $"{key}={value}".ToLower()) 59 | >> (fun h -> String.Join("&", h)) 60 | 61 | let headerPrefix = "X-Custom-Value" 62 | let customHeaders = [ for i in 1..10 -> $"{headerPrefix}{i}", $"{i}" ] 63 | let expected = headersToString customHeaders 64 | 65 | use server = 66 | GET 67 | >=> request (fun r -> 68 | r.headers 69 | |> List.filter (fun (k, _) -> k.StartsWith(headerPrefix, StringComparison.OrdinalIgnoreCase)) 70 | |> headersToString 71 | |> OK 72 | ) 73 | |> serve 74 | 75 | http { 76 | GET(url @"") 77 | headers customHeaders 78 | } 79 | |> Request.send 80 | |> Response.toText 81 | |> should equal expected 82 | 83 | 84 | [] 85 | let ``Response Decompression`` () = 86 | // Why so many chars? Suave has a configured minimum size for compression of 500 bytes! 87 | let responseText = 88 | @" 89 | Hello World Hello World Hello World Hello World Hello World Hello World 90 | Hello World Hello World Hello World Hello World Hello World Hello World 91 | Hello World Hello World Hello World Hello World Hello World Hello World 92 | Hello World Hello World Hello World Hello World Hello World Hello World 93 | Hello World Hello World Hello World Hello World Hello World Hello World 94 | Hello World Hello World Hello World Hello World Hello World Hello World 95 | Hello World Hello World Hello World Hello World Hello World Hello World 96 | Hello World Hello World Hello World Hello World Hello World Hello World 97 | Hello World Hello World Hello World Hello World Hello World Hello World 98 | Hello World Hello World Hello World Hello World Hello World Hello World 99 | " 100 | 101 | use server = 102 | GET 103 | >=> request (fun r -> 104 | // setting the mime type to "text/html" will cause the response to be decompressed: 105 | // https://suave.io/files.html 106 | responseText |> OK >=> setMimeType "text/html" 107 | ) 108 | |> serve 109 | 110 | let baseRequest = 111 | http { 112 | GET(url @"") 113 | AcceptEncoding "gzip, deflate" 114 | } 115 | 116 | // automatic response decompression (default) 117 | baseRequest |> Request.send |> Response.toText |> should equal responseText 118 | 119 | // manual decompression 120 | baseRequest { config_noDecompression } 121 | |> Request.send 122 | |> Response.toBytes 123 | |> fun responseContent -> 124 | use ms = new MemoryStream(responseContent) 125 | use gs = new Compression.GZipStream(ms, Compression.CompressionMode.Decompress) 126 | use sr = new StreamReader(gs, encoding = Text.Encoding.UTF8) 127 | sr.ReadToEnd() 128 | |> should equal responseText 129 | 130 | 131 | //let [] ``Auto Redirects``() = 132 | // http { 133 | // GET (url @"") 134 | // config_transformHttpClientHandler (fun handler -> 135 | // handler.AllowAutoRedirect <- false 136 | // handler 137 | // ) 138 | // } 139 | // |> Response.toText 140 | // |> should equal "hello world" 141 | 142 | 143 | 144 | // TODO: 145 | 146 | // let [] ``Http reauest message can be modified``() = 147 | // use server = GET >=> request (header "accept-language" >> OK) |> serve 148 | 149 | // let lang = "fr" 150 | // http { 151 | // GET (url @"") 152 | // transformHttpRequestMessage (fun httpRequestMessage -> 153 | // httpRequestMessage 154 | // ) 155 | // } 156 | // |> toText 157 | // |> should equal lang 158 | 159 | // TODO: Timeout 160 | // TODO: ToFormattedText 161 | // TODO: transformHttpRequestMessage 162 | // TODO: transformHttpClient 163 | // TODO: Cookie tests (test the overloads) 164 | -------------------------------------------------------------------------------- /src/Tests/Multipart.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Multipart 2 | 3 | open System.IO 4 | 5 | open FsUnit 6 | open FsHttp 7 | open FsHttp.Tests.Server 8 | open FsHttp.Tests.TestHelper 9 | 10 | open NUnit.Framework 11 | 12 | open Suave 13 | open Suave.Operators 14 | open Suave.Filters 15 | open Suave.Successful 16 | 17 | 18 | [] 19 | let ``POST Multipart form data`` () = 20 | use server = 21 | POST 22 | >=> request (fun r -> 23 | let fileContents = 24 | r.files |> List.map (fun f -> File.ReadAllText f.tempFilePath) |> joinLines 25 | 26 | let multipartContents = 27 | r.multiPartFields |> List.map (fun (k, v) -> k + "=" + v) |> joinLines 28 | 29 | [ fileContents; multipartContents ] |> joinLines |> OK 30 | ) 31 | |> serve 32 | 33 | http { 34 | POST(url @"") 35 | multipart 36 | filePart "Resources/uploadFile.txt" 37 | filePart "Resources/uploadFile2.txt" 38 | textPart "das" "hurz1" 39 | textPart "Lamm" "hurz2" 40 | textPart "schrie" "hurz3" 41 | } 42 | |> Request.send 43 | |> Response.toText 44 | |> should 45 | equal 46 | (joinLines [ 47 | "I'm a chicken and I can fly!" 48 | "Lemonade was a popular drink, and it still is." 49 | "hurz1=das" 50 | "hurz2=Lamm" 51 | "hurz3=schrie" 52 | ]) 53 | 54 | 55 | [] 56 | let ``Explicitly specified content type part is dominant`` () = 57 | 58 | let explicitContentType1 = "text/whatever1" 59 | let explicitContentType2 = "text/whatever2" 60 | 61 | use server = 62 | POST 63 | >=> request (fun r -> r.files |> List.map (fun f -> f.mimeType) |> String.concat "," |> OK) 64 | |> serve 65 | 66 | http { 67 | POST(url @"") 68 | multipart 69 | 70 | filePart "Resources/uploadFile.txt" 71 | ContentType explicitContentType1 72 | 73 | filePart "Resources/uploadFile2.txt" 74 | ContentType explicitContentType2 75 | } 76 | |> Request.send 77 | |> Response.toText 78 | |> should equal (explicitContentType1 + "," + explicitContentType2) 79 | 80 | 81 | [] 82 | let ``POST Multipart part binary with optional filename`` () = 83 | let fileName1 = "fileName1" 84 | let fileName2 = "fileName2" 85 | let fileName3 = "fileName3" 86 | 87 | use server = 88 | POST 89 | >=> request (fun r -> 90 | let fileNames = r.files |> List.map (fun f -> f.fileName) |> joinLines 91 | 92 | fileNames |> OK 93 | ) 94 | |> serve 95 | 96 | http { 97 | POST(url @"") 98 | multipart 99 | 100 | binaryPart [| byte 0xff |] "photo" fileName1 101 | ContentType "image/jpeg" 102 | 103 | binaryPart [| byte 0xff |] "photo" fileName2 104 | ContentType "image/jpeg" 105 | 106 | binaryPart [| byte 0xff |] "photo" fileName3 107 | ContentType "image/jpeg" 108 | 109 | binaryPart [| byte 0xff |] "photo" 110 | ContentType "image/jpeg" 111 | } 112 | |> Request.send 113 | |> Response.toText 114 | |> should equal (joinLines [ fileName1; fileName2; fileName3 ]) 115 | 116 | 117 | [] 118 | let ``POST Multipart textPart with optional filename`` () = 119 | let fileName1 = "fileName1" 120 | let fileName2 = "fileName2" 121 | let fileName3 = "fileName3" 122 | 123 | use server = 124 | POST 125 | >=> request (fun r -> 126 | let fileNames = r.files |> List.map (fun f -> f.fileName) |> joinLines 127 | 128 | fileNames |> OK 129 | ) 130 | |> serve 131 | 132 | http { 133 | POST(url @"") 134 | multipart 135 | 136 | textPart "the_value" "the_name" fileName1 137 | ContentType "application/json" 138 | 139 | filePart "Resources/uploadFile.txt" "theName" fileName2 140 | ContentType "application/json" 141 | 142 | binaryPart [| byte 0xff |] "theName" fileName3 143 | ContentType "application/json" 144 | } 145 | |> Request.send 146 | |> Response.toText 147 | |> should equal (joinLines [ fileName1; fileName2; fileName3 ]) 148 | -------------------------------------------------------------------------------- /src/Tests/Printing.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Printing 2 | 3 | open FsUnit 4 | open FsHttp 5 | open FsHttp.Tests.Server 6 | 7 | open NUnit.Framework 8 | 9 | open Suave 10 | open Suave.Operators 11 | open Suave.Filters 12 | open Suave.Successful 13 | 14 | [] 15 | let ``Print Config on Request`` () = 16 | use server = GET >=> request (fun r -> r.rawQuery |> OK) |> serve 17 | 18 | let resonseLines = 19 | http { 20 | GET(url @"?test=Hallo") 21 | print_withResponseBodyLength 3 22 | } 23 | |> Request.send 24 | |> Response.print 25 | |> String.replace "\r" "" 26 | |> String.split '\n' 27 | 28 | printfn "%A" resonseLines 29 | 30 | resonseLines 31 | |> List.skipWhile (fun x -> x <> "RESPONSE") 32 | |> List.skip 1 33 | |> List.skipWhile (fun x -> x <> FsHttp.Print.contentIndicator) 34 | |> List.skip 1 35 | |> List.head 36 | |> should equal "tes" 37 | -------------------------------------------------------------------------------- /src/Tests/Proxies.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.Proxies 2 | 3 | open System 4 | open System.Net 5 | 6 | open FsUnit 7 | open FsHttp 8 | open FsHttp.Tests.Server 9 | 10 | open NUnit.Framework 11 | 12 | open Suave 13 | open Suave.Operators 14 | open Suave.Filters 15 | open Suave.Successful 16 | open Suave.Response 17 | open Suave.Writers 18 | 19 | 20 | [] 21 | let ``Proxy usage works`` () = 22 | use server = GET >=> OK "proxified" |> serve 23 | 24 | http { 25 | GET "http://google.com" 26 | config_proxy (url "") 27 | } 28 | |> Request.send 29 | |> Response.toText 30 | |> should equal "proxified" 31 | 32 | [] 33 | let ``Proxy usage with credentials works`` () = 34 | use server = 35 | GET 36 | >=> request (fun r -> 37 | printfn "Headers: %A" r.headers 38 | 39 | match r.header "Proxy-Authorization" with 40 | | Choice1Of2 cred -> cred |> OK 41 | | _ -> 42 | response HTTP_407 (Text.Encoding.UTF8.GetBytes "No credentials") 43 | >=> setHeader "Proxy-Authenticate" "Basic" 44 | ) 45 | |> serve 46 | 47 | let credentials = NetworkCredential("test", "password") 48 | 49 | http { 50 | GET "http://google.com" 51 | config_proxyWithCredentials (url "") credentials 52 | } 53 | |> Request.send 54 | |> Response.toText 55 | |> should 56 | equal 57 | ("Basic " 58 | + ("test:password" |> Text.Encoding.UTF8.GetBytes |> Convert.ToBase64String)) 59 | -------------------------------------------------------------------------------- /src/Tests/Resources/uploadFile.txt: -------------------------------------------------------------------------------- 1 | I'm a chicken and I can fly! -------------------------------------------------------------------------------- /src/Tests/Resources/uploadFile2.txt: -------------------------------------------------------------------------------- 1 | Lemonade was a popular drink, and it still is. -------------------------------------------------------------------------------- /src/Tests/Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | Debug;Release 6 | 7 | 8 | 9 | 10 | 11 | 12 | Always 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/Tests/UrlsAndQuery.fs: -------------------------------------------------------------------------------- 1 | module FsHttp.Tests.``Urls and Query`` 2 | 3 | open FsUnit 4 | open FsHttp 5 | open FsHttp.Tests.TestHelper 6 | open FsHttp.Tests.Server 7 | 8 | open NUnit.Framework 9 | 10 | open Suave 11 | open Suave.Operators 12 | open Suave.Filters 13 | open Suave.Successful 14 | 15 | 16 | [] 17 | let ``Multiline urls`` () = 18 | use server = 19 | GET >=> request (fun r -> (query "q1" r) + "_" + (query "q2" r) |> OK) |> serve 20 | 21 | http { 22 | GET( 23 | url 24 | @" 25 | ?q1=Query1 26 | &q2=Query2" 27 | ) 28 | } 29 | |> Request.send 30 | |> Response.toText 31 | |> should equal "Query1_Query2" 32 | 33 | 34 | [] 35 | let ``Comments in urls are discarded`` () = 36 | use server = 37 | GET 38 | >=> request (fun r -> (query "q1" r) + "_" + (query "q2" r) + "_" + (query "q3" r) |> OK) 39 | |> serve 40 | 41 | http { 42 | GET( 43 | url 44 | @" 45 | ?q1=Query1 46 | //&q2=Query2 47 | &q3=Query3" 48 | ) 49 | } 50 | |> Request.send 51 | |> Response.toText 52 | |> should equal ("Query1_" + keyNotFoundString + "_Query3") 53 | 54 | 55 | [] 56 | let ``Empty query params`` () = 57 | use server = GET >=> request (fun _ -> "" |> OK) |> serve 58 | 59 | http { 60 | GET(url "") 61 | query [] 62 | } 63 | |> Request.send 64 | |> Response.toText 65 | |> should equal "" 66 | 67 | 68 | [] 69 | let ``Merge query params with url params`` () = 70 | use server = 71 | GET >=> request (fun r -> (query "q1" r) + "_" + (query "q2" r) |> OK) |> serve 72 | 73 | http { 74 | GET(url "?q1=Query1") 75 | query [ "q2", "Query2" ] 76 | } 77 | |> Request.send 78 | |> Response.toText 79 | |> should equal "Query1_Query2" 80 | 81 | 82 | [] 83 | let ``Query params`` () = 84 | use server = 85 | GET >=> request (fun r -> (query "q1" r) + "_" + (query "q2" r) |> OK) |> serve 86 | 87 | http { 88 | GET(url "") 89 | query [ "q1", "Query1"; "q2", "Query2" ] 90 | } 91 | |> Request.send 92 | |> Response.toText 93 | |> should equal "Query1_Query2" 94 | 95 | 96 | [] 97 | let ``Query params encoding`` () = 98 | use server = GET >=> request (fun r -> query "q1" r |> OK) |> serve 99 | 100 | http { 101 | GET(url "") 102 | query [ "q1", "<>" ] 103 | } 104 | |> Request.send 105 | |> Response.toText 106 | |> should equal "<>" 107 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | dotnet fsi build.fsx test --------------------------------------------------------------------------------