├── .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 | [](https://github.com/schlenkr/FsHttp/actions/workflows/build-and-test.yml)
4 | [](https://www.nuget.org/packages/FsHttp)
5 | [](https://www.nuget.org/packages/FsHttp)
6 |
7 |
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 |
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 |
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
--------------------------------------------------------------------------------