[
  {
    "path": ".github/workflows/deploy_web_demo.yml",
    "content": "name: Deploy web demo\n\non:\n  # We only run this on merges to master\n  push:\n    branches: [\"master\"]\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n# to only run when you do a new github release, comment out above part and uncomment the below trigger.\n# on:\n#   release:\n#     types: [\"published\"]\n\npermissions:\n  contents: write # for committing to gh-pages branch\n\n# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.\n# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.\nconcurrency:\n  group: \"pages\"\n  cancel-in-progress: false\n\nenv:\n  # web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses,\n  # as well as by the wasm32-backend of the wgpu crate.\n  # https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html\n  # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html\n  RUSTFLAGS: --cfg=web_sys_unstable_apis --cfg getrandom_backend=\"wasm_js\" -D warnings\n  RUSTDOCFLAGS: -D warnings\n\njobs:\n  # Single deploy job since we're just deploying\n  deploy:\n    name: Deploy web demo\n\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          target: wasm32-unknown-unknown\n          toolchain: 1.88.0\n          override: true\n\n      - uses: Swatinem/rust-cache@v2\n        with:\n          prefix-key: \"web-demo-\"\n\n      - name: \"Install wasmopt / binaryen\"\n        run: |\n          sudo apt-get update && sudo apt-get install binaryen\n\n      - run: |\n          example_eframe/build_web.sh --release\n\n      - name: Deploy\n        uses: JamesIves/github-pages-deploy-action@v4\n        with:\n          folder: web_demo\n          # this option will not maintain any history of your previous pages deployment\n          # set to false if you want all page build to be committed to your gh-pages branch history\n          single-commit: true\n"
  },
  {
    "path": ".github/workflows/rust.yml",
    "content": "on: [push, pull_request]\n\nname: CI\n\nenv:\n  # This is required to enable the web_sys clipboard API which egui_web uses\n  # https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html\n  # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html\n  RUSTFLAGS: --cfg=web_sys_unstable_apis --deny warnings\n  RUSTDOCFLAGS: --deny warnings\n\njobs:\n  check_native:\n    name: cargo check --all-features\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: 1.88.0\n          override: true\n      - uses: actions-rs/cargo@v1\n        with:\n          command: check\n          args: --all-features\n\n  check_web:\n    name: cargo check web --all-features\n    runs-on: ubuntu-latest\n    env:\n      RUSTFLAGS: --cfg=web_sys_unstable_apis --cfg getrandom_backend=\"wasm_js\" --deny warnings\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: 1.88.0\n          override: true\n      - run: rustup target add wasm32-unknown-unknown\n      - uses: actions-rs/cargo@v1\n        with:\n          command: check\n          args: --lib --target wasm32-unknown-unknown --all-features\n\n  test:\n    name: cargo test\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: 1.88.0\n          override: true\n      - uses: actions-rs/cargo@v1\n        with:\n          command: test\n          args: --all-features -p ehttp\n\n  fmt:\n    name: cargo fmt\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: 1.88.0\n          override: true\n      - run: rustup component add rustfmt\n      - uses: actions-rs/cargo@v1\n        with:\n          command: fmt\n          args: --all -- --check\n\n  clippy:\n    name: cargo clippy\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: 1.88.0\n          override: true\n      - run: rustup component add clippy\n      - uses: actions-rs/cargo@v1\n        with:\n          command: clippy\n          args: --all-targets --all-features --  --deny warnings -W clippy::all\n\n  doc:\n    name: cargo doc\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: 1.88.0\n          override: true\n      - run: cargo doc -p ehttp --lib --no-deps --all-features\n\n  doc_web:\n    name: cargo doc web\n    runs-on: ubuntu-latest\n    env:\n      RUSTFLAGS: --cfg=web_sys_unstable_apis --cfg getrandom_backend=\"wasm_js\" --deny warnings\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: 1.88.0\n          override: true\n      - run: rustup target add wasm32-unknown-unknown\n      - run: cargo doc -p ehttp --target wasm32-unknown-unknown --lib --no-deps --all-features\n\n  cargo-deny:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: EmbarkStudios/cargo-deny-action@v2\n        with:\n          rust-version: \"1.88.0\"\n          log-level: error\n          command: check\n"
  },
  {
    "path": ".gitignore",
    "content": "**/target\n**/target_ra\n/.vscode\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# egui changelog\n\nAll notable changes to the `ehttp` crate will be documented in this file.\n\n\n## Unreleased\n\n\n## 0.7.1 - 2026-03-23\n* Relax version requirements on `wasm-bindgen`, `js-sys`, `web-sys`, and `wasm-streams`\n\n\n## 0.7.0 - 2026-03-23\n* Add builder methods to `Request` ([#77](https://github.com/emilk/ehttp/pull/77))\n* Add `Request::with_credentials` for web ([#62](https://github.com/emilk/ehttp/pull/62))\n* Update to `ureq` 3 ([#76](https://github.com/emilk/ehttp/pull/76))\n* Update MSRV to 1.88 ([#78](https://github.com/emilk/ehttp/pull/78))\n\n\n## 0.6.0 - 2025-12-05\n* Support configurable timeouts ([#71](https://github.com/emilk/ehttp/pull/71))\n* Add Node.js support ([#58](https://github.com/emilk/ehttp/pull/58))\n* Include `mode` on native too ([#54](https://github.com/emilk/ehttp/pull/54))\n\n\n## 0.5.0 - 2024-02-16\n* Support multipart and JSON ([#47](https://github.com/emilk/ehttp/pull/47), [#49](https://github.com/emilk/ehttp/pull/49))\n* Added CORS `Mode` property to `Request` on web ([#52](https://github.com/emilk/ehttp/pull/52))\n* Don't add `web-sys` in native builds ([#48](https://github.com/emilk/ehttp/pull/48))\n\n\n## 0.4.0 - 2024-01-17\n* Allow duplicated headers in requests and responses ([#46](https://github.com/emilk/ehttp/pull/46))\n* Support HEAD requests ([#45](https://github.com/emilk/ehttp/pull/45))\n* Add missing web-sys feature ([#42](https://github.com/emilk/ehttp/pull/42))\n* Update MSRV to 1.72.0 ([#44](https://github.com/emilk/ehttp/pull/44))\n\n\n## 0.3.1 - 2023-09-27\n* Improve opaque network error message on web ([#33](https://github.com/emilk/ehttp/pull/33)).\n\n\n## 0.3.0 - 2023-06-15\n* Add `ehttp::streaming`, for streaming HTTP requests ([#28](https://github.com/emilk/ehttp/pull/28)).\n* Add cross-platform `fetch_async` ([#25](https://github.com/emilk/ehttp/pull/25)).\n* Nicer formatted error messages on web.\n* Implement `Clone` and `Debug` for `Request` ([#17](https://github.com/emilk/ehttp/pull/17)).\n\n\n## 0.2.0 - 2022-01-15\n* `Response::text` and `Response::content_type` no longer allocates.\n* Rename `ehttp::Request::create_headers_map` to `ehttp::headers`.\n* `Request::post` now expects `Vec<u8>`.\n\n\n## 0.1.0 - 2021-09-03 - First release\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nmembers = [\n    \"ehttp\",\n    \"example_eframe\",\n]\n"
  },
  {
    "path": "LICENSE-APACHE",
    "content": "                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don't include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "LICENSE-MIT",
    "content": "Copyright (c) 2018-2021 Emil Ernerfeldt <emil.ernerfeldt@gmail.com>\n\nPermission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# ehttp: a minimal Rust HTTP client for both native and WASM\n\n[![Latest version](https://img.shields.io/crates/v/ehttp.svg)](https://crates.io/crates/ehttp)\n[![Documentation](https://docs.rs/ehttp/badge.svg)](https://docs.rs/ehttp)\n[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)\n[![Build Status](https://github.com/emilk/ehttp/workflows/CI/badge.svg)](https://github.com/emilk/ehttp/actions?workflow=CI)\n![MIT](https://img.shields.io/badge/license-MIT-blue.svg)\n![Apache](https://img.shields.io/badge/license-Apache-blue.svg)\n\nIf you want to do HTTP requests and are targeting both native, web (WASM) and NodeJS (since v18.0), then this is the crate for you!\n\n[You can try the web demo here](https://emilk.github.io/ehttp/index.html) (works in any browser with WASM and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).\n\n## Usage\n``` rust\nlet request = ehttp::Request::get(\"https://www.example.com\");\nehttp::fetch(request, move |result: ehttp::Result<ehttp::Response>| {\n    println!(\"Status code: {:?}\", result.unwrap().status);\n});\n```\n\nThe given callback is called when the request is completed.\nYou can communicate the results back to the main thread using something like:\n\n* Channels (e.g. [`std::sync::mpsc::channel`](https://doc.rust-lang.org/std/sync/mpsc/fn.channel.html)).\n* `Arc<Mutex<_>>`\n* [`poll_promise::Promise`](https://docs.rs/poll-promise)\n* [`eventuals::Eventual`](https://docs.rs/eventuals/latest/eventuals/struct.Eventual.html)\n* [`tokio::sync::watch::channel`](https://docs.rs/tokio/latest/tokio/sync/watch/fn.channel.html)\n\nThere is also a streaming version under `ehttp::fetch::streaming`, hidden behind the `streaming` feature flag.\n"
  },
  {
    "path": "RELEASES.md",
    "content": "# Release Checklist\n\n* [ ] Update `CHANGELOG.md`\n* [ ] Bump version numbers\n* [ ] `git commit -m 'Release 0.x.0 - summary'`\n* [ ] `cargo publish`\n* [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - summary'`\n* [ ] `git pull --tags && git tag -d latest && git tag -a latest -m 'Latest release' && git push --tags origin latest --force`\n* [ ] `git push && git push --tags`\n* [ ] Do a GitHub release: https://github.com/emilk/ehttp/releases/new\n* [ ] Wait for documentation to build: https://docs.rs/releases/queue\n* [ ] Post on Twitter\n"
  },
  {
    "path": "cargo_deny.sh",
    "content": "#!/usr/bin/env bash\n\nset -eu\nscript_path=$( cd \"$(dirname \"${BASH_SOURCE[0]}\")\" ; pwd -P )\ncd \"$script_path\"\nset -x\n\n# cargo install cargo-deny\ncargo deny --all-features --log-level error --target aarch64-apple-darwin check\ncargo deny --all-features --log-level error --target i686-pc-windows-gnu check\ncargo deny --all-features --log-level error --target i686-pc-windows-msvc check\ncargo deny --all-features --log-level error --target i686-unknown-linux-gnu check\ncargo deny --all-features --log-level error --target wasm32-unknown-unknown check\ncargo deny --all-features --log-level error --target x86_64-apple-darwin check\ncargo deny --all-features --log-level error --target x86_64-pc-windows-gnu check\ncargo deny --all-features --log-level error --target x86_64-pc-windows-msvc check\ncargo deny --all-features --log-level error --target x86_64-unknown-linux-gnu check\ncargo deny --all-features --log-level error --target x86_64-unknown-linux-musl check\ncargo deny --all-features --log-level error --target x86_64-unknown-redox check\n"
  },
  {
    "path": "check.sh",
    "content": "#!/usr/bin/env bash\n# This scripts runs various CI-like checks in a convenient way.\n\nset -eu\nscript_path=$( cd \"$(dirname \"${BASH_SOURCE[0]}\")\" ; pwd -P )\ncd \"$script_path\"\nset -x\n\nexport RUSTFLAGS=\"--deny warnings\"\n\n# https://github.com/ericseppanen/cargo-cranky/issues/8\nexport RUSTDOCFLAGS=\"--deny warnings --deny rustdoc::missing_crate_level_docs\"\n\ntypos # cargo install typos-cli\n\ncargo fmt --all -- --check\ncargo cranky --quiet --all-targets --all-features -- --deny warnings\ncargo test --quiet --all-targets --all-features\ncargo test --quiet --doc --all-features # checks all doc-tests\n\ncargo cranky --quiet --lib --target wasm32-unknown-unknown --all-features\n\ncargo doc --quiet --no-deps --all-features\ncargo doc --quiet --document-private-items --no-deps --all-features\n\ncargo deny --all-features --log-level error check\n\necho \"All checks passed!\"\n"
  },
  {
    "path": "deny.toml",
    "content": "# https://github.com/EmbarkStudios/cargo-deny\n#\n# cargo-deny checks our dependency tree for copy-left licenses,\n# duplicate dependencies, and rustsec advisories (https://rustsec.org/advisories).\n#\n# Install: `cargo install cargo-deny`\n# Check: `cargo deny check`.\n\n\n# Note: running just `cargo deny check` without a `--target` can result in\n# false positives due to https://github.com/EmbarkStudios/cargo-deny/issues/324\n[graph]\ntargets = [\n  { triple = \"aarch64-apple-darwin\" },\n  { triple = \"i686-pc-windows-gnu\" },\n  { triple = \"i686-pc-windows-msvc\" },\n  { triple = \"i686-unknown-linux-gnu\" },\n  { triple = \"wasm32-unknown-unknown\" },\n  { triple = \"x86_64-apple-darwin\" },\n  { triple = \"x86_64-pc-windows-gnu\" },\n  { triple = \"x86_64-pc-windows-msvc\" },\n  { triple = \"x86_64-unknown-linux-gnu\" },\n  { triple = \"x86_64-unknown-linux-musl\" },\n  { triple = \"x86_64-unknown-redox\" },\n]\nall-features = true\n\n\n[advisories]\nversion = 2\nignore = []\n\n\n[bans]\nmultiple-versions = \"deny\"\nwildcards = \"deny\"\ndeny = [\n  { name = \"openssl\", reason = \"Use rustls\" },\n  { name = \"openssl-sys\", reason = \"Use rustls\" },\n]\n\nskip = []\nskip-tree = [\n  { name = \"eframe\" },         # dev-dependency\n  { name = \"example_eframe\" }, # example\n]\n\n\n[licenses]\nversion = 2\nprivate = { ignore = true }\nconfidence-threshold = 0.93 # We want really high confidence when inferring licenses from text\nallow = [\n  \"Apache-2.0 WITH LLVM-exception\", # https://spdx.org/licenses/LLVM-exception.html\n  \"Apache-2.0\",                     # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)\n  \"BSD-2-Clause\",                   # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd)\n  \"BSD-3-Clause\",                   # https://tldrlegal.com/license/bsd-3-clause-license-(revised)\n  \"BSL-1.0\",                        # https://tldrlegal.com/license/boost-software-license-1.0-explained\n  \"CC0-1.0\",                        # https://creativecommons.org/publicdomain/zero/1.0/\n  \"CDLA-Permissive-2.0\",            # https://spdx.org/licenses/CDLA-Permissive-2.0.html\n  \"ISC\",                            # https://www.tldrlegal.com/license/isc-license\n  \"MIT-0\",                          # https://choosealicense.com/licenses/mit-0/\n  \"MIT\",                            # https://tldrlegal.com/license/mit-license\n  \"MPL-2.0\",                        # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11\n  \"OFL-1.1\",                        # https://spdx.org/licenses/OFL-1.1.html\n  \"OpenSSL\",                        # https://www.openssl.org/source/license.html\n  \"Ubuntu-font-1.0\",                # https://ubuntu.com/legal/font-licence\n  \"Unicode-3.0\",                    # https://www.unicode.org/license.txt\n  \"Unicode-DFS-2016\",               # https://spdx.org/licenses/Unicode-DFS-2016.html\n  \"Zlib\",                           # https://tldrlegal.com/license/zlib-libpng-license-(zlib)\n]\nexceptions = []\n\n[[licenses.clarify]]\nname = \"webpki\"\nexpression = \"ISC\"\nlicense-files = [{ path = \"LICENSE\", hash = 0x001c7e6c }]\n\n[[licenses.clarify]]\nname = \"rustls-webpki\"\nexpression = \"ISC\"\nlicense-files = [{ path = \"LICENSE\", hash = 0x001c7e6c }]\n\n[[licenses.clarify]]\nname = \"ring\"\nexpression = \"MIT AND ISC AND OpenSSL\"\nlicense-files = [{ path = \"LICENSE\", hash = 0xbd0eed23 }]\n\n\n[sources]\nunknown-registry = \"deny\"\nunknown-git = \"deny\"\n"
  },
  {
    "path": "ehttp/Cargo.toml",
    "content": "[package]\nname = \"ehttp\"\nversion = \"0.7.1\"\nauthors = [\"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>\"]\ndescription = \"Minimal HTTP client for both native and WASM\"\nedition = \"2018\"\nrust-version = \"1.88\"\nhomepage = \"https://github.com/emilk/ehttp\"\nlicense = \"MIT OR Apache-2.0\"\nreadme = \"../README.md\"\nrepository = \"https://github.com/emilk/ehttp\"\ncategories = [\"network-programming\", \"wasm\", \"web-programming\"]\nkeywords = [\"http\", \"wasm\", \"native\", \"web\"]\ninclude = [\"../LICENSE-APACHE\", \"../LICENSE-MIT\", \"**/*.rs\", \"Cargo.toml\"]\n\n[package.metadata.docs.rs]\nall-features = true\n\n[lib]\n\n\n[features]\ndefault = []\n\n## Support json fetch\njson = [\"dep:serde\", \"dep:serde_json\"]\n\n## Support multipart fetch\nmultipart = [\"dep:getrandom\", \"dep:mime\", \"dep:mime_guess\", \"dep:rand\"]\n\n## Support `fetch_async` on native\nnative-async = [\"async-channel\"]\n\n## Support streaming fetch\nstreaming = [\"dep:wasm-streams\", \"dep:futures-util\"]\n\n\n[dependencies]\ndocument-features = \"0.2.12\"\n\n# Multipart request\nmime = { version = \"0.3.17\", optional = true }\nmime_guess = { version = \"2.0.5\", optional = true }\nrand = { version = \"0.10.0\", optional = true }\n\n# Json request\nserde = { version = \"1.0.228\", optional = true }\nserde_json = { version = \"1.0.149\", optional = true }\n\n# For compiling natively:\n[target.'cfg(not(target_arch = \"wasm32\"))'.dependencies]\n# ureq = { version = \"2.0\", default-features = false, features = [\"gzip\", \"tls_native_certs\"] }\nureq = \"3.3.0\"\nasync-channel = { version = \"2.5.0\", optional = true }\n\n# For compiling to web:\n[target.'cfg(target_arch = \"wasm32\")'.dependencies]\njs-sys = \"0.3.85\"\nwasm-bindgen = \"0.2.108\"\nwasm-bindgen-futures = \"0.4.45\"\n\n# Multipart request\ngetrandom = { version = \"0.4.2\", features = [\"wasm_js\"], optional = true }\n\n# Streaming response\nfutures-util = { version = \"0.3.32\", optional = true }\nwasm-streams = { version = \"0.4.2\", optional = true }\n\nweb-sys = { version = \"0.3.85\", features = [\n  \"AbortSignal\",\n  \"console\",\n  \"Headers\",\n  \"ReadableStream\",\n  \"Request\",\n  \"RequestInit\",\n  \"RequestMode\",\n  \"RequestCredentials\",\n  \"Response\",\n  \"Window\",\n] }\n"
  },
  {
    "path": "ehttp/src/lib.rs",
    "content": "//! Minimal HTTP client for both native and WASM.\n//!\n//! Example:\n//! ```\n//! let request = ehttp::Request::get(\"https://www.example.com\");\n//! ehttp::fetch(request, move |result: ehttp::Result<ehttp::Response>| {\n//!     println!(\"Status code: {:?}\", result.unwrap().status);\n//! });\n//! ```\n//!\n//! The given callback is called when the request is completed.\n//! You can communicate the results back to the main thread using something like:\n//!\n//! * Channels (e.g. [`std::sync::mpsc::channel`](https://doc.rust-lang.org/std/sync/mpsc/fn.channel.html)).\n//! * `Arc<Mutex<_>>`\n//! * [`poll_promise::Promise`](https://docs.rs/poll-promise)\n//! * [`eventuals::Eventual`](https://docs.rs/eventuals/latest/eventuals/struct.Eventual.html)\n//! * [`tokio::sync::watch::channel`](https://docs.rs/tokio/latest/tokio/sync/watch/fn.channel.html)\n//!\n//! ## Feature flags\n#![doc = document_features::document_features!()]\n//!\n\n/// Performs an HTTP request and calls the given callback when done.\n///\n/// `Ok` is returned if we get a response, even if it's a 404.\n///\n/// `Err` can happen for a number of reasons:\n/// * No internet connection\n/// * Connection timed out\n/// * DNS resolution failed\n/// * Firewall or proxy blocked the request\n/// * Server is not reachable\n/// * The URL is invalid\n/// * Server's SSL cert is invalid\n/// * CORS errors\n/// * The initial GET which returned HTML contained CSP headers to block access to the resource\n/// * A browser extension blocked the request (e.g. ad blocker)\n/// * …\npub fn fetch(request: Request, on_done: impl 'static + Send + FnOnce(Result<Response>)) {\n    #[cfg(not(target_arch = \"wasm32\"))]\n    native::fetch(request, Box::new(on_done));\n\n    #[cfg(target_arch = \"wasm32\")]\n    web::fetch(request, Box::new(on_done));\n}\n\n/// Performs an `async` HTTP request.\n///\n/// Available on following platforms:\n/// - web\n/// - native behind the `native-async` feature.\n///\n/// `Ok` is returned if we get a response, even if it's a 404.\n///\n/// `Err` can happen for a number of reasons:\n/// * No internet connection\n/// * Connection timed out\n/// * DNS resolution failed\n/// * Firewall or proxy blocked the request\n/// * Server is not reachable\n/// * The URL is invalid\n/// * Server's SSL cert is invalid\n/// * CORS errors\n/// * The initial GET which returned HTML contained CSP headers to block access to the resource\n/// * A browser extension blocked the request (e.g. ad blocker)\n/// * …\n#[cfg(any(target_arch = \"wasm32\", feature = \"native-async\"))]\npub async fn fetch_async(request: Request) -> Result<Response> {\n    #[cfg(not(target_arch = \"wasm32\"))]\n    return native::fetch_async(request).await;\n\n    #[cfg(target_arch = \"wasm32\")]\n    return web::fetch_async(&request).await;\n}\n\nmod types;\npub use types::{Error, Headers, Method, PartialResponse, Request, Response, Result};\n\n#[cfg(target_arch = \"wasm32\")]\npub use types::Credentials;\n#[cfg(target_arch = \"wasm32\")]\npub use types::Mode;\n\n#[cfg(not(target_arch = \"wasm32\"))]\nmod native;\n#[cfg(not(target_arch = \"wasm32\"))]\npub use native::fetch_blocking;\n\n#[cfg(target_arch = \"wasm32\")]\nmod web;\n#[cfg(target_arch = \"wasm32\")]\npub use web::spawn_future;\n\n#[cfg(feature = \"streaming\")]\npub mod streaming;\n\n#[cfg(feature = \"multipart\")]\npub mod multipart;\n\n#[deprecated = \"Use ehttp::Headers::new\"]\npub fn headers(headers: &[(&str, &str)]) -> Headers {\n    Headers::new(headers)\n}\n"
  },
  {
    "path": "ehttp/src/multipart.rs",
    "content": "//! Multipart HTTP request for both native and WASM.\n//!\n//! Requires the `multipart` feature to be enabled.\n//!\n//! Example:\n//! ```\n//! use std::io::Cursor;\n//! use ehttp::multipart::MultipartBuilder;\n//! let url = \"https://www.example.com\";\n//! let request = ehttp::Request::post_multipart(\n//!     url,\n//!     MultipartBuilder::new()\n//!         .add_text(\"label\", \"lorem ipsum\")\n//!         .add_stream(\n//!             &mut Cursor::new(vec![0, 0, 0, 0]),\n//!             \"4_empty_bytes\",\n//!             Some(\"4_empty_bytes.png\"),\n//!             None,\n//!         )\n//!         .unwrap(),\n//! );\n//! ehttp::fetch(request, |result| {});\n//! ```\n//! Taken from ureq_multipart 1.1.1\n//!\n\nuse mime::Mime;\nuse rand::RngExt as _;\n\nuse std::io::{self, Read, Write as _};\n\nconst BOUNDARY_LEN: usize = 29;\n\nfn random_alphanumeric(len: usize) -> String {\n    rand::rng()\n        .sample_iter(rand::distr::Uniform::new_inclusive(0, 9).unwrap())\n        .take(len)\n        .map(|num: i32| num.to_string())\n        .collect()\n}\n\n#[derive(Debug)]\n/// The Builder for the multipart\npub struct MultipartBuilder {\n    boundary: String,\n    inner: Vec<u8>,\n    data_written: bool,\n}\n\nimpl Default for MultipartBuilder {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl MultipartBuilder {\n    /// creates a new MultipartBuilder with empty inner\n    pub fn new() -> Self {\n        Self {\n            boundary: random_alphanumeric(BOUNDARY_LEN),\n            inner: Vec::new(),\n            data_written: false,\n        }\n    }\n\n    /// add text field\n    ///\n    /// * name field name\n    /// * text field text value\n    pub fn add_text(mut self, name: &str, text: &str) -> Self {\n        self.write_field_headers(name, None, None);\n        self.inner.extend(text.as_bytes());\n        self\n    }\n\n    /// add file\n    ///\n    /// * name file field name\n    /// * path the sending file path\n    #[cfg(not(target_arch = \"wasm32\"))]\n    pub fn add_file<P: AsRef<std::path::Path>>(self, name: &str, path: P) -> io::Result<Self> {\n        fn mime_filename(path: &std::path::Path) -> (Mime, Option<&str>) {\n            let content_type = mime_guess::from_path(path);\n            let filename = path.file_name().and_then(|filename| filename.to_str());\n            (content_type.first_or_octet_stream(), filename)\n        }\n\n        let path = path.as_ref();\n        let (content_type, filename) = mime_filename(path);\n        let mut file = std::fs::File::open(path)?;\n        self.add_stream(&mut file, name, filename, Some(content_type))\n    }\n\n    /// add some stream\n    pub fn add_stream<S: Read>(\n        mut self,\n        stream: &mut S,\n        name: &str,\n        filename: Option<&str>,\n        content_type: Option<Mime>,\n    ) -> io::Result<Self> {\n        // This is necessary to make sure it is interpreted as a file on the server end.\n        let content_type = Some(content_type.unwrap_or(mime::APPLICATION_OCTET_STREAM));\n        self.write_field_headers(name, filename, content_type);\n        io::copy(stream, &mut self.inner)?;\n        Ok(self)\n    }\n\n    fn write_boundary(&mut self) {\n        if self.data_written {\n            self.inner.write_all(b\"\\r\\n\").unwrap();\n        }\n\n        write!(\n            self.inner,\n            \"-----------------------------{}\\r\\n\",\n            self.boundary\n        )\n        .unwrap()\n    }\n\n    fn write_field_headers(\n        &mut self,\n        name: &str,\n        filename: Option<&str>,\n        content_type: Option<Mime>,\n    ) {\n        self.write_boundary();\n        if !self.data_written {\n            self.data_written = true;\n        }\n        write!(\n            self.inner,\n            \"Content-Disposition: form-data; name=\\\"{name}\\\"\"\n        )\n        .unwrap();\n        if let Some(filename) = filename {\n            write!(self.inner, \"; filename=\\\"{filename}\\\"\").unwrap();\n        }\n        if let Some(content_type) = content_type {\n            write!(self.inner, \"\\r\\nContent-Type: {content_type}\").unwrap();\n        }\n        self.inner.write_all(b\"\\r\\n\\r\\n\").unwrap();\n    }\n\n    /// general multipart data\n    ///\n    /// # Return\n    /// * (content_type,post_data)\n    ///    * content_type http header content type\n    ///    * post_data ureq.req.send_send_bytes(&post_data)\n    ///\n    pub fn finish(mut self) -> (String, Vec<u8>) {\n        if self.data_written {\n            self.inner.write_all(b\"\\r\\n\").unwrap();\n        }\n\n        // always write the closing boundary, even for empty bodies\n        write!(\n            self.inner,\n            \"-----------------------------{}--\\r\\n\",\n            self.boundary\n        )\n        .unwrap();\n        (\n            format!(\n                \"multipart/form-data; boundary=---------------------------{}\",\n                self.boundary\n            ),\n            self.inner,\n        )\n    }\n}\n"
  },
  {
    "path": "ehttp/src/native.rs",
    "content": "use crate::{Method, Request, Response};\n\n#[cfg(feature = \"native-async\")]\nuse async_channel::{Receiver, Sender};\n\n/// Performs a  HTTP request and blocks the thread until it is done.\n///\n/// Only available when compiling for native.\n///\n/// NOTE: `Ok(…)` is returned on network error.\n///\n/// `Ok` is returned if we get a response, even if it's a 404.\n///\n/// `Err` can happen for a number of reasons:\n/// * No internet connection\n/// * Connection timed out\n/// * DNS resolution failed\n/// * Firewall or proxy blocked the request\n/// * Server is not reachable\n/// * The URL is invalid\n/// * Server's SSL cert is invalid\n/// * CORS errors\n/// * The initial GET which returned HTML contained CSP headers to block access to the resource\n/// * A browser extension blocked the request (e.g. ad blocker)\n/// * …\npub fn fetch_blocking(request: &Request) -> crate::Result<Response> {\n    let mut resp = request.fetch_raw_native(true)?;\n\n    let ok = resp.status().is_success();\n    use ureq::ResponseExt as _;\n    let url = resp.get_uri().to_string();\n    let status = resp.status().as_u16();\n    let status_text = resp\n        .status()\n        .canonical_reason()\n        .unwrap_or(\"ERROR\")\n        .to_string();\n    let mut headers = crate::Headers::default();\n    for (k, v) in resp.headers().iter() {\n        headers.insert(\n            k,\n            v.to_str()\n                .map_err(|e| format!(\"Failed to convert header value to string: {e}\"))?,\n        );\n    }\n    headers.sort(); // It reads nicer, and matches web backend.\n\n    let mut reader = resp.body_mut().as_reader();\n    let mut bytes = vec![];\n    use std::io::Read as _;\n    if let Err(err) = reader.read_to_end(&mut bytes) {\n        if err.kind() == std::io::ErrorKind::Other && request.method == Method::HEAD {\n            match err.downcast::<ureq::Error>() {\n                Ok(ureq::Error::Decompress(_, io_err))\n                    if io_err.kind() == std::io::ErrorKind::UnexpectedEof =>\n                {\n                    // We don't really expect a body for HEAD requests, so this is fine.\n                }\n                Ok(err_inner) => return Err(format!(\"Failed to read response body: {err_inner}\")),\n                Err(err) => {\n                    return Err(format!(\"Failed to read response body: {err}\"));\n                }\n            }\n        } else {\n            return Err(format!(\"Failed to read response body: {err}\"));\n        }\n    }\n\n    let response = Response {\n        url,\n        ok,\n        status,\n        status_text,\n        headers,\n        bytes,\n    };\n    Ok(response)\n}\n\n// ----------------------------------------------------------------------------\n\npub(crate) fn fetch(request: Request, on_done: Box<dyn FnOnce(crate::Result<Response>) + Send>) {\n    std::thread::Builder::new()\n        .name(\"ehttp\".to_owned())\n        .spawn(move || on_done(fetch_blocking(&request)))\n        .expect(\"Failed to spawn ehttp thread\");\n}\n\n#[cfg(feature = \"native-async\")]\npub(crate) async fn fetch_async(request: Request) -> crate::Result<Response> {\n    let (tx, rx): (\n        Sender<crate::Result<Response>>,\n        Receiver<crate::Result<Response>>,\n    ) = async_channel::bounded(1);\n\n    fetch(\n        request,\n        Box::new(move |received| tx.send_blocking(received).unwrap()),\n    );\n    rx.recv().await.map_err(|err| err.to_string())?\n}\n"
  },
  {
    "path": "ehttp/src/streaming/mod.rs",
    "content": "//! Streaming HTTP client for both native and WASM.\n//!\n//! Requires the `streaming` feature to be enabled.\n//!\n//! Example:\n//! ```\n//! let your_chunk_handler = std::sync::Arc::new(|chunk: Vec<u8>| {\n//!     if chunk.is_empty() {\n//!         return std::ops::ControlFlow::Break(());\n//!     }\n//!\n//!     println!(\"received chunk: {} bytes\", chunk.len());\n//!     std::ops::ControlFlow::Continue(())\n//! });\n//!\n//! let url = \"https://www.example.com\";\n//! let request = ehttp::Request::get(url);\n//! ehttp::streaming::fetch(request, move |result: ehttp::Result<ehttp::streaming::Part>| {\n//!     let part = match result {\n//!         Ok(part) => part,\n//!         Err(err) => {\n//!             eprintln!(\"an error occurred while streaming `{url}`: {err}\");\n//!             return std::ops::ControlFlow::Break(());\n//!         }\n//!     };\n//!\n//!     match part {\n//!         ehttp::streaming::Part::Response(response) => {\n//!             println!(\"Status code: {:?}\", response.status);\n//!             if response.ok {\n//!                 std::ops::ControlFlow::Continue(())\n//!             } else {\n//!                 std::ops::ControlFlow::Break(())\n//!             }\n//!         }\n//!         ehttp::streaming::Part::Chunk(chunk) => {\n//!             your_chunk_handler(chunk)\n//!         }\n//!     }\n//! });\n//! ```\n//!\n//! The streaming fetch works like the non-streaming fetch, but instead\n//! of receiving the response in full, you receive parts of the response\n//! as they are streamed in.\n\nuse std::ops::ControlFlow;\n\nuse crate::Request;\n\n/// Performs a HTTP requests and calls the given callback once for the initial response,\n/// and then once for each chunk in the response body.\n///\n/// You can abort the fetch by returning [`ControlFlow::Break`] from the callback.\npub fn fetch(\n    request: Request,\n    on_data: impl 'static + Send + Fn(crate::Result<types::Part>) -> ControlFlow<()>,\n) {\n    #[cfg(not(target_arch = \"wasm32\"))]\n    native::fetch_streaming(request, Box::new(on_data));\n\n    #[cfg(target_arch = \"wasm32\")]\n    web::fetch_streaming(request, Box::new(on_data));\n}\n\n#[cfg(not(target_arch = \"wasm32\"))]\nmod native;\n#[cfg(not(target_arch = \"wasm32\"))]\npub use native::fetch_streaming_blocking;\n\n#[cfg(target_arch = \"wasm32\")]\nmod web;\n#[cfg(target_arch = \"wasm32\")]\npub use web::fetch_async_streaming;\n\nmod types;\n\npub use self::types::Part;\n"
  },
  {
    "path": "ehttp/src/streaming/native.rs",
    "content": "use std::ops::ControlFlow;\n\nuse crate::{Method, Request};\n\nuse super::Part;\nuse crate::types::PartialResponse;\n\npub fn fetch_streaming_blocking(\n    request: Request,\n    on_data: Box<dyn Fn(crate::Result<Part>) -> ControlFlow<()> + Send>,\n) {\n    let resp = request.fetch_raw_native(false);\n\n    let mut resp = match resp {\n        Ok(t) => t,\n        Err(e) => {\n            let _ = on_data(Err(e.to_string()));\n            return;\n        }\n    };\n\n    let ok = resp.status().is_success();\n    use ureq::ResponseExt as _;\n    let url = resp.get_uri().to_string();\n    let status = resp.status().as_u16();\n    let status_text = resp\n        .status()\n        .canonical_reason()\n        .unwrap_or(\"ERROR\")\n        .to_string();\n    let mut headers = crate::Headers::default();\n    for (k, v) in resp.headers().iter() {\n        headers.insert(\n            k,\n            match v.to_str() {\n                Ok(t) => t,\n                Err(e) => {\n                    let _ = on_data(Err(e.to_string()));\n                    break;\n                }\n            },\n        );\n    }\n    headers.sort(); // It reads nicer, and matches web backend.\n\n    let response = PartialResponse {\n        url,\n        ok,\n        status,\n        status_text,\n        headers,\n    };\n    if on_data(Ok(Part::Response(response))).is_break() {\n        return;\n    };\n\n    let mut reader = resp.body_mut().as_reader();\n    loop {\n        let mut buf = vec![0; 2048];\n        use std::io::Read;\n        match reader.read(&mut buf) {\n            Ok(n) if n > 0 => {\n                // clone data from buffer and clear it\n                let chunk = buf[..n].to_vec();\n                if on_data(Ok(Part::Chunk(chunk))).is_break() {\n                    return;\n                };\n            }\n            Ok(_) => {\n                let _ = on_data(Ok(Part::Chunk(vec![])));\n                break;\n            }\n            Err(err) => {\n                if err.kind() == std::io::ErrorKind::Other && request.method == Method::HEAD {\n                    match err.downcast::<ureq::Error>() {\n                        Ok(ureq::Error::Decompress(_, io_err))\n                            if io_err.kind() == std::io::ErrorKind::UnexpectedEof =>\n                        {\n                            // We don't really expect a body for HEAD requests, so this is fine.\n                            let _ = on_data(Ok(Part::Chunk(vec![])));\n                            break;\n                        }\n                        Ok(err_inner) => {\n                            let _ =\n                                on_data(Err(format!(\"Failed to read response body: {err_inner}\")));\n                            return;\n                        }\n                        Err(err) => {\n                            let _ = on_data(Err(format!(\"Failed to read response body: {err}\")));\n                            return;\n                        }\n                    }\n                } else {\n                    let _ = on_data(Err(format!(\"Failed to read response body: {err}\")));\n                    return;\n                }\n            }\n        };\n    }\n}\n\npub(crate) fn fetch_streaming(\n    request: Request,\n    on_data: Box<dyn Fn(crate::Result<Part>) -> ControlFlow<()> + Send>,\n) {\n    std::thread::Builder::new()\n        .name(\"ehttp\".to_owned())\n        .spawn(move || fetch_streaming_blocking(request, on_data))\n        .expect(\"Failed to spawn ehttp thread\");\n}\n"
  },
  {
    "path": "ehttp/src/streaming/types.rs",
    "content": "use crate::types::PartialResponse;\n\n/// A piece streamed by [`crate::streaming::fetch`].\npub enum Part {\n    /// The header of the response.\n    ///\n    /// The `on_data` callback receives this only once.\n    Response(PartialResponse),\n\n    /// A single chunk of the response data.\n    ///\n    /// If the chunk is empty, that means the `on_data` callback will not receive any more data.\n    Chunk(Vec<u8>),\n}\n"
  },
  {
    "path": "ehttp/src/streaming/web.rs",
    "content": "use std::ops::ControlFlow;\n\nuse futures_util::Stream;\nuse futures_util::StreamExt;\nuse wasm_bindgen::prelude::*;\n\nuse crate::web::{fetch_base, get_response_base, spawn_future, string_from_fetch_error};\nuse crate::Request;\n\nuse super::types::Part;\n\n/// Only available when compiling for web.\n///\n/// NOTE: `Ok(…)` is returned on network error.\n/// `Err` is only for failure to use the fetch API.\n#[cfg(feature = \"streaming\")]\npub async fn fetch_async_streaming(\n    request: &Request,\n) -> crate::Result<impl Stream<Item = crate::Result<Part>>> {\n    let stream = fetch_jsvalue_stream(request)\n        .await\n        .map_err(string_from_fetch_error)?;\n    Ok(stream.map(|result| result.map_err(string_from_fetch_error)))\n}\n\n#[cfg(feature = \"streaming\")]\nasync fn fetch_jsvalue_stream(\n    request: &Request,\n) -> Result<impl Stream<Item = Result<Part, JsValue>>, JsValue> {\n    use js_sys::Uint8Array;\n\n    let response = fetch_base(request).await?;\n    let body = wasm_streams::ReadableStream::from_raw(\n        response.body().ok_or(\"response has no body\")?.dyn_into()?,\n    );\n\n    // returns a `Part::Response` followed by all the chunks in `body` as `Part::Chunk`\n    Ok(\n        futures_util::stream::once(futures_util::future::ready(Ok(Part::Response(\n            get_response_base(&response)?,\n        ))))\n        .chain(\n            body.into_stream()\n                .map(|value| value.map(|value| Part::Chunk(Uint8Array::new(&value).to_vec()))),\n        ),\n    )\n}\n\npub(crate) fn fetch_streaming(\n    request: Request,\n    on_data: Box<dyn Fn(crate::Result<Part>) -> ControlFlow<()> + Send>,\n) {\n    spawn_future(async move {\n        let mut stream = match fetch_jsvalue_stream(&request).await {\n            Ok(stream) => stream,\n            Err(e) => {\n                let _ = on_data(Err(string_from_fetch_error(e)));\n                return;\n            }\n        };\n\n        while let Some(chunk) = stream.next().await {\n            match chunk {\n                Ok(chunk) => {\n                    if on_data(Ok(chunk)).is_break() {\n                        return;\n                    }\n                }\n                Err(e) => {\n                    let _ = on_data(Err(string_from_fetch_error(e)));\n                    return;\n                }\n            }\n        }\n\n        let _ = on_data(Ok(Part::Chunk(vec![])));\n    })\n}\n"
  },
  {
    "path": "ehttp/src/types.rs",
    "content": "use std::time::Duration;\n\n#[cfg(feature = \"json\")]\nuse serde::Serialize;\n\n#[cfg(feature = \"multipart\")]\nuse crate::multipart::MultipartBuilder;\n\n/// Headers in a [`Request`] or [`Response`].\n///\n/// Note that the same header key can appear twice.\n#[derive(Clone, Debug, Default)]\npub struct Headers {\n    /// Name-value pairs.\n    pub headers: Vec<(String, String)>,\n}\n\nimpl Headers {\n    /// ```\n    /// use ehttp::Request;\n    /// let request = Request {\n    ///     headers: ehttp::Headers::new(&[\n    ///         (\"Accept\", \"*/*\"),\n    ///         (\"Content-Type\", \"text/plain; charset=utf-8\"),\n    ///     ]),\n    ///     ..Request::get(\"https://www.example.com\")\n    /// };\n    /// ```\n    pub fn new(headers: &[(&str, &str)]) -> Self {\n        Self {\n            headers: headers\n                .iter()\n                .map(|e| (e.0.to_owned(), e.1.to_owned()))\n                .collect(),\n        }\n    }\n\n    /// Will add the key/value pair to the headers.\n    ///\n    /// If the key already exists, it will also be kept,\n    /// so the same key can appear twice.\n    pub fn insert(&mut self, key: impl ToString, value: impl ToString) {\n        self.headers.push((key.to_string(), value.to_string()));\n    }\n\n    /// Get the value of the first header with the given key.\n    ///\n    /// The lookup is case-insensitive.\n    pub fn get(&self, key: &str) -> Option<&str> {\n        let key = key.to_string().to_lowercase();\n        self.headers\n            .iter()\n            .find(|(k, _)| k.to_lowercase() == key)\n            .map(|(_, v)| v.as_str())\n    }\n\n    /// Get all the values that match the given key.\n    ///\n    /// The lookup is case-insensitive.\n    pub fn get_all(&self, key: &str) -> impl Iterator<Item = &str> {\n        let key = key.to_string().to_lowercase();\n        self.headers\n            .iter()\n            .filter(move |(k, _)| k.to_lowercase() == key)\n            .map(|(_, v)| v.as_str())\n    }\n\n    /// Sort the headers by key.\n    ///\n    /// This makes the headers easier to read when printed out.\n    ///\n    /// `ehttp` will sort the headers in the responses.\n    pub fn sort(&mut self) {\n        self.headers.sort_by(|a, b| a.0.cmp(&b.0));\n    }\n}\n\nimpl<const N: usize> From<&[(&str, &str); N]> for Headers {\n    fn from(headers: &[(&str, &str); N]) -> Self {\n        Self::new(headers.as_slice())\n    }\n}\n\nimpl IntoIterator for Headers {\n    type Item = (String, String);\n    type IntoIter = std::vec::IntoIter<Self::Item>;\n\n    fn into_iter(self) -> Self::IntoIter {\n        self.headers.into_iter()\n    }\n}\n\nimpl<'h> IntoIterator for &'h Headers {\n    type Item = &'h (String, String);\n    type IntoIter = std::slice::Iter<'h, (String, String)>;\n\n    fn into_iter(self) -> Self::IntoIter {\n        self.headers.iter()\n    }\n}\n\n// ----------------------------------------------------------------------------\n\n/// Determine if cross-origin requests lead to valid responses.\n///\n/// Based on <https://developer.mozilla.org/en-US/docs/Web/API/Request/mode>\n#[cfg(target_arch = \"wasm32\")]\n#[derive(Default, Clone, Copy, Debug)]\npub enum Mode {\n    /// If a request is made to another origin with this mode set, the result is an error.\n    SameOrigin = 0,\n\n    /// The request will not include the Origin header in a request.\n    /// The server's response will be opaque, meaning that JavaScript code cannot access its contents\n    NoCors = 1,\n\n    /// Includes an Origin header in the request and expects the server to respond with an\n    /// \"Access-Control-Allow-Origin\" header that indicates whether the request is allowed.\n    #[default]\n    Cors = 2,\n\n    /// A mode for supporting navigation\n    Navigate = 3,\n}\n\n#[cfg(target_arch = \"wasm32\")]\nimpl From<Mode> for web_sys::RequestMode {\n    fn from(mode: Mode) -> Self {\n        match mode {\n            Mode::SameOrigin => web_sys::RequestMode::SameOrigin,\n            Mode::NoCors => web_sys::RequestMode::NoCors,\n            Mode::Cors => web_sys::RequestMode::Cors,\n            Mode::Navigate => web_sys::RequestMode::Navigate,\n        }\n    }\n}\n\n// ----------------------------------------------------------------------------\n\n/// Determines whether or not the browser sends credentials with the request, as well as whether any Set-Cookie response headers are respected.\n///\n/// Based on <https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials>\n#[cfg(target_arch = \"wasm32\")]\n#[derive(Default, Clone, Copy, Debug)]\npub enum Credentials {\n    /// Never send credentials in the request or include credentials in the response.\n    #[default]\n    Omit = 0,\n\n    /// Only send and include credentials for same-origin requests.\n    SameOrigin = 1,\n\n    /// Always include credentials, even for cross-origin requests.\n    Include = 2,\n}\n\n#[cfg(target_arch = \"wasm32\")]\nimpl From<Credentials> for web_sys::RequestCredentials {\n    fn from(credentials: Credentials) -> Self {\n        match credentials {\n            Credentials::Omit => web_sys::RequestCredentials::Omit,\n            Credentials::SameOrigin => web_sys::RequestCredentials::SameOrigin,\n            Credentials::Include => web_sys::RequestCredentials::Include,\n        }\n    }\n}\n\n/// A simple HTTP request.\n#[derive(Clone, Debug)]\npub struct Request {\n    /// \"GET\", \"POST\", …\n    pub method: Method,\n\n    /// https://…\n    pub url: String,\n\n    /// The data you send with e.g. \"POST\".\n    pub body: Vec<u8>,\n\n    /// (\"Accept\", \"*/*\"), …\n    pub headers: Headers,\n\n    /// Cancel the request if it doesn't complete fast enough.\n    pub timeout: Option<Duration>,\n\n    /// Request mode used on fetch.\n    ///\n    /// Used on Web to control CORS.\n    #[cfg(target_arch = \"wasm32\")]\n    pub mode: Mode,\n\n    /// Credential options for fetch.\n    ///\n    /// Only applies to the web backend.\n    #[cfg(target_arch = \"wasm32\")]\n    pub credentials: Credentials,\n}\n\nimpl Request {\n    /// The default timeout for requests (30 seconds).\n    pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);\n\n    /// Create a new request with the given method, url, and headers.\n    #[expect(clippy::needless_pass_by_value)]\n    pub fn new(method: Method, url: impl ToString, headers: impl Into<Headers>) -> Self {\n        Self {\n            method,\n            url: url.to_string(),\n            body: vec![],\n            headers: headers.into(),\n            timeout: Some(Self::DEFAULT_TIMEOUT),\n            #[cfg(target_arch = \"wasm32\")]\n            mode: Mode::default(),\n            #[cfg(target_arch = \"wasm32\")]\n            credentials: Credentials::default(),\n        }\n    }\n\n    /// Create a `GET` request with the given url.\n    pub fn get(url: impl ToString) -> Self {\n        Self::new(Method::GET, url, &[(\"Accept\", \"*/*\")])\n    }\n\n    /// Create a `HEAD` request with the given url.\n    pub fn head(url: impl ToString) -> Self {\n        Self::new(Method::HEAD, url, &[(\"Accept\", \"*/*\")])\n    }\n\n    /// Create a `POST` request with the given url and body.\n    pub fn post(url: impl ToString, body: Vec<u8>) -> Self {\n        Self::new(\n            Method::POST,\n            url,\n            &[\n                (\"Accept\", \"*/*\"),\n                (\"Content-Type\", \"text/plain; charset=utf-8\"),\n            ],\n        )\n        .with_body(body)\n    }\n\n    /// Create a 'PUT' request with the given url and body.\n    pub fn put(url: impl ToString, body: Vec<u8>) -> Self {\n        Self::new(\n            Method::PUT,\n            url,\n            &[\n                (\"Accept\", \"*/*\"),\n                (\"Content-Type\", \"text/plain; charset=utf-8\"),\n            ],\n        )\n        .with_body(body)\n    }\n\n    /// Create a 'DELETE' request with the given url.\n    pub fn delete(url: &str) -> Self {\n        Self::new(Method::DELETE, url, &[(\"Accept\", \"*/*\")])\n    }\n\n    /// Multipart HTTP for both native and WASM.\n    ///\n    /// Requires the `multipart` feature to be enabled.\n    ///\n    /// Example:\n    /// ```\n    /// use std::io::Cursor;\n    /// use ehttp::multipart::MultipartBuilder;\n    /// let url = \"https://www.example.com\";\n    /// let request = ehttp::Request::post_multipart(\n    ///     url,\n    ///     MultipartBuilder::new()\n    ///         .add_text(\"label\", \"lorem ipsum\")\n    ///         .add_stream(\n    ///             &mut Cursor::new(vec![0, 0, 0, 0]),\n    ///             \"4_empty_bytes\",\n    ///             Some(\"4_empty_bytes.png\"),\n    ///             None,\n    ///         )\n    ///         .unwrap(),\n    /// );\n    /// ehttp::fetch(request, |result| {});\n    /// ```\n    #[cfg(feature = \"multipart\")]\n    pub fn post_multipart(url: impl ToString, builder: MultipartBuilder) -> Self {\n        let (content_type, data) = builder.finish();\n        Self::new(\n            Method::POST,\n            url,\n            Headers::new(&[(\"Accept\", \"*/*\"), (\"Content-Type\", content_type.as_str())]),\n        )\n        .with_body(data)\n    }\n\n    #[cfg(feature = \"multipart\")]\n    #[deprecated(note = \"Renamed to `post_multipart`\")]\n    pub fn multipart(url: impl ToString, builder: MultipartBuilder) -> Self {\n        Self::post_multipart(url, builder)\n    }\n\n    #[cfg(feature = \"json\")]\n    /// Create a `POST` request with the given url and json body.\n    pub fn post_json<T>(url: impl ToString, body: &T) -> serde_json::error::Result<Self>\n    where\n        T: ?Sized + Serialize,\n    {\n        Ok(Self::new(\n            Method::POST,\n            url,\n            &[(\"Accept\", \"*/*\"), (\"Content-Type\", \"application/json\")],\n        )\n        .with_body(serde_json::to_string(body)?.into_bytes()))\n    }\n\n    #[cfg(feature = \"json\")]\n    #[deprecated(note = \"Renamed to `post_json`\")]\n    pub fn json<T>(url: impl ToString, body: &T) -> serde_json::error::Result<Self>\n    where\n        T: ?Sized + Serialize,\n    {\n        Self::post_json(url, body)\n    }\n\n    #[cfg(feature = \"json\")]\n    /// Create a 'PUT' request with the given url and json body.\n    pub fn put_json<T>(url: impl ToString, body: &T) -> serde_json::error::Result<Self>\n    where\n        T: ?Sized + Serialize,\n    {\n        Ok(Self::new(\n            Method::PUT,\n            url,\n            &[(\"Accept\", \"*/*\"), (\"Content-Type\", \"application/json\")],\n        )\n        .with_body(serde_json::to_string(body)?.into_bytes()))\n    }\n\n    /// Set the HTTP method.\n    pub fn with_method(mut self, method: Method) -> Self {\n        self.method = method;\n        self\n    }\n\n    /// Set the URL.\n    pub fn with_url(mut self, url: impl ToString) -> Self {\n        self.url = url.to_string();\n        self\n    }\n\n    /// Set the request body.\n    pub fn with_body(mut self, body: Vec<u8>) -> Self {\n        self.body = body;\n        self\n    }\n\n    /// Replace all headers.\n    pub fn with_headers(mut self, headers: Headers) -> Self {\n        self.headers = headers;\n        self\n    }\n\n    /// Append a single header to the request.\n    pub fn with_header(mut self, key: impl ToString, value: impl ToString) -> Self {\n        self.headers.insert(key, value);\n        self\n    }\n\n    /// Set the request timeout, or `None` to disable it.\n    pub fn with_timeout(mut self, timeout: Option<Duration>) -> Self {\n        self.timeout = timeout;\n        self\n    }\n\n    /// Set the request mode (controls CORS behavior on web).\n    #[cfg(target_arch = \"wasm32\")]\n    pub fn with_mode(mut self, mode: Mode) -> Self {\n        self.mode = mode;\n        self\n    }\n\n    /// Set whether credentials are sent with the request (web only).\n    #[cfg(target_arch = \"wasm32\")]\n    pub fn with_credentials(mut self, credentials: Credentials) -> Self {\n        self.credentials = credentials;\n        self\n    }\n\n    /// Fetch the ureq response from a page\n    #[cfg(not(target_arch = \"wasm32\"))]\n    pub fn fetch_raw_native(&self, with_timeout: bool) -> Result<ureq::http::Response<ureq::Body>> {\n        if self.method.contains_body() {\n            let mut req = match self.method {\n                Method::POST => ureq::post(&self.url),\n                Method::PATCH => ureq::patch(&self.url),\n                Method::PUT => ureq::put(&self.url),\n                // These three are the only requests which contain a body, no other requests will be matched\n                _ => unreachable!(), // because of the `.contains_body()` call\n            };\n\n            for (k, v) in &self.headers {\n                req = req.header(k, v);\n            }\n\n            req = {\n                if with_timeout {\n                    req.config()\n                } else {\n                    req.config().timeout_recv_body(self.timeout)\n                }\n                .http_status_as_error(false)\n                .build()\n            };\n\n            if self.body.is_empty() {\n                req.send_empty()\n            } else {\n                req.send(&self.body)\n            }\n        } else {\n            let mut req = match self.method {\n                Method::GET => ureq::get(&self.url),\n                Method::DELETE => ureq::delete(&self.url),\n                Method::CONNECT => ureq::connect(&self.url),\n                Method::HEAD => ureq::head(&self.url),\n                Method::OPTIONS => ureq::options(&self.url),\n                Method::TRACE => ureq::trace(&self.url),\n                // Include all other variants rather than a catch all here to prevent confusion if another variant were to be added\n                Method::PATCH | Method::POST | Method::PUT => unreachable!(), // because of the `.contains_body()` call\n            };\n\n            req = req\n                .config()\n                .timeout_recv_body(self.timeout)\n                .http_status_as_error(false)\n                .build();\n\n            for (k, v) in &self.headers {\n                req = req.header(k, v);\n            }\n\n            if self.body.is_empty() {\n                req.call()\n            } else {\n                req.force_send_body().send(&self.body)\n            }\n        }\n        .map_err(|err| err.to_string())\n    }\n}\n\n/// Response from a completed HTTP request.\n#[derive(Clone)]\npub struct Response {\n    /// The URL we ended up at. This can differ from the request url when we have followed redirects.\n    pub url: String,\n\n    /// Did we get a 2xx response code?\n    pub ok: bool,\n\n    /// Status code (e.g. `404` for \"File not found\").\n    pub status: u16,\n\n    /// Status text (e.g. \"File not found\" for status code `404`).\n    pub status_text: String,\n\n    /// The returned headers.\n    pub headers: Headers,\n\n    /// The raw bytes of the response body.\n    pub bytes: Vec<u8>,\n}\n\nimpl Response {\n    pub fn text(&self) -> Option<&str> {\n        std::str::from_utf8(&self.bytes).ok()\n    }\n\n    #[cfg(feature = \"json\")]\n    /// Convenience for getting json body\n    pub fn json<T: serde::de::DeserializeOwned>(&self) -> serde_json::Result<T> {\n        serde_json::from_slice(self.bytes.as_slice())\n    }\n\n    /// Convenience for getting the `content-type` header.\n    pub fn content_type(&self) -> Option<&str> {\n        self.headers.get(\"content-type\")\n    }\n}\n\nimpl std::fmt::Debug for Response {\n    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let Self {\n            url,\n            ok,\n            status,\n            status_text,\n            headers,\n            bytes,\n        } = self;\n\n        fmt.debug_struct(\"Response\")\n            .field(\"url\", url)\n            .field(\"ok\", ok)\n            .field(\"status\", status)\n            .field(\"status_text\", status_text)\n            .field(\"headers\", headers)\n            .field(\"bytes\", &format!(\"{} bytes\", bytes.len()))\n            .finish_non_exhaustive()\n    }\n}\n\n/// An HTTP response status line and headers used for the [`streaming`](crate::streaming) API.\n#[derive(Clone, Debug)]\npub struct PartialResponse {\n    /// The URL we ended up at. This can differ from the request url when we have followed redirects.\n    pub url: String,\n\n    /// Did we get a 2xx response code?\n    pub ok: bool,\n\n    /// Status code (e.g. `404` for \"File not found\").\n    pub status: u16,\n\n    /// Status text (e.g. \"File not found\" for status code `404`).\n    pub status_text: String,\n\n    /// The returned headers.\n    pub headers: Headers,\n}\n\nimpl PartialResponse {\n    pub fn complete(self, bytes: Vec<u8>) -> Response {\n        let Self {\n            url,\n            ok,\n            status,\n            status_text,\n            headers,\n        } = self;\n        Response {\n            url,\n            ok,\n            status,\n            status_text,\n            headers,\n            bytes,\n        }\n    }\n}\n\n/// A description of an error.\n///\n/// This is only used when we fail to make a request.\n/// Any response results in `Ok`, including things like 404 (file not found).\npub type Error = String;\n\n/// A type-alias for `Result<T, ehttp::Error>`.\npub type Result<T> = std::result::Result<T, Error>;\n\n/// An [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods)\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum Method {\n    GET,\n    HEAD,\n    POST,\n    PUT,\n    DELETE,\n    CONNECT,\n    OPTIONS,\n    TRACE,\n    PATCH,\n}\n\nimpl Method {\n    /// Whether ureq creates a `RequestBuilder<WithBody>` or `RequestBuilder<WithoutBody>`\n    pub fn contains_body(&self) -> bool {\n        use Method::*;\n        match self {\n            // Methods that are created with a body\n            POST | PATCH | PUT => true,\n            // Everything else\n            _ => false,\n        }\n    }\n\n    /// Convert an HTTP method string (\"GET\", \"HEAD\") to its enum variant\n    pub fn parse(string: &str) -> Result<Self> {\n        use Method::*;\n        match string {\n            \"GET\" => Ok(GET),\n            \"HEAD\" => Ok(HEAD),\n            \"POST\" => Ok(POST),\n            \"PUT\" => Ok(PUT),\n            \"DELETE\" => Ok(DELETE),\n            \"CONNECT\" => Ok(CONNECT),\n            \"OPTIONS\" => Ok(OPTIONS),\n            \"TRACE\" => Ok(TRACE),\n            \"PATCH\" => Ok(PATCH),\n            _ => Err(Error::from(\"Failed to parse HTTP method\")),\n        }\n    }\n\n    pub fn as_str(&self) -> &'static str {\n        use Method::*;\n        match self {\n            GET => \"GET\",\n            HEAD => \"HEAD\",\n            POST => \"POST\",\n            PUT => \"PUT\",\n            DELETE => \"DELETE\",\n            CONNECT => \"CONNECT\",\n            OPTIONS => \"OPTIONS\",\n            TRACE => \"TRACE\",\n            PATCH => \"PATCH\",\n        }\n    }\n}\n"
  },
  {
    "path": "ehttp/src/web.rs",
    "content": "use wasm_bindgen::prelude::*;\nuse wasm_bindgen_futures::JsFuture;\n\nuse crate::types::PartialResponse;\nuse crate::{Request, Response};\n\n/// Binds the JavaScript `fetch` method for use in both Node.js (>= v18.0) and browser environments.\n#[wasm_bindgen]\nextern \"C\" {\n    #[wasm_bindgen(js_name = fetch)]\n    fn fetch_with_request(input: &web_sys::Request) -> js_sys::Promise;\n}\n\n/// Only available when compiling for web.\n///\n/// NOTE: `Ok(…)` is returned on network error.\n///\n/// `Err` can happen for a number of reasons:\n/// * No internet connection\n/// * Connection timed out\n/// * DNS resolution failed\n/// * Firewall or proxy blocked the request\n/// * Server is not reachable\n/// * The URL is invalid\n/// * Server's SSL cert is invalid\n/// * CORS errors\n/// * The initial GET which returned HTML contained CSP headers to block access to the resource\n/// * A browser extension blocked the request (e.g. ad blocker)\n/// * …\npub async fn fetch_async(request: &Request) -> crate::Result<Response> {\n    fetch_jsvalue(request)\n        .await\n        .map_err(string_from_fetch_error)\n}\n\n/// This should only be used to handle opaque exceptions thrown by the `fetch` call.\npub(crate) fn string_from_fetch_error(value: JsValue) -> String {\n    value.as_string().unwrap_or_else(|| {\n        // TypeError means that this is an opaque `network error`, as defined by the spec:\n        // https://fetch.spec.whatwg.org/\n        if value.has_type::<js_sys::TypeError>() {\n            web_sys::console::error_1(&value);\n            \"Failed to fetch, check the developer console for details\".to_owned()\n        } else {\n            format!(\"{value:#?}\")\n        }\n    })\n}\n\npub(crate) async fn fetch_base(request: &Request) -> Result<web_sys::Response, JsValue> {\n    let opts = web_sys::RequestInit::new();\n    opts.set_method(request.method.as_str());\n    opts.set_mode(request.mode.into());\n    opts.set_credentials(request.credentials.into());\n\n    if !request.body.is_empty() {\n        let body_bytes: &[u8] = &request.body;\n        let body_array: js_sys::Uint8Array = body_bytes.into();\n        let js_value: &JsValue = body_array.as_ref();\n        opts.set_body(js_value);\n    }\n\n    let js_request = web_sys::Request::new_with_str_and_init(&request.url, &opts)?;\n\n    for (k, v) in &request.headers {\n        js_request.headers().set(k, v)?;\n    }\n\n    let response = JsFuture::from(fetch_with_request(&js_request)).await?;\n    let response: web_sys::Response = response.dyn_into()?;\n\n    Ok(response)\n}\n\npub(crate) fn get_response_base(response: &web_sys::Response) -> Result<PartialResponse, JsValue> {\n    // https://developer.mozilla.org/en-US/docs/Web/API/Headers\n    // \"Note: When Header values are iterated over, […] values from duplicate header names are combined.\"\n    // TODO: support duplicate header names\n    let js_headers: web_sys::Headers = response.headers();\n    let js_iter = js_sys::try_iter(&js_headers)\n        .expect(\"headers try_iter\")\n        .expect(\"headers have an iterator\");\n\n    let mut headers = crate::Headers::default();\n    for item in js_iter {\n        let item = item.expect(\"headers iterator\");\n        let array: js_sys::Array = item.into();\n        let v: Vec<JsValue> = array.to_vec();\n\n        let key = v[0]\n            .as_string()\n            .ok_or_else(|| JsValue::from_str(\"headers name\"))?;\n        let value = v[1]\n            .as_string()\n            .ok_or_else(|| JsValue::from_str(\"headers value\"))?;\n\n        headers.insert(key, value);\n    }\n\n    Ok(PartialResponse {\n        url: response.url(),\n        ok: response.ok(),\n        status: response.status(),\n        status_text: response.status_text(),\n        headers,\n    })\n}\n\n/// NOTE: `Ok(…)` is returned on network error.\n/// `Err` is only for failure to use the fetch API.\nasync fn fetch_jsvalue(request: &Request) -> Result<Response, JsValue> {\n    let response = fetch_base(request).await?;\n\n    let array_buffer = JsFuture::from(response.array_buffer()?).await?;\n    let uint8_array = js_sys::Uint8Array::new(&array_buffer);\n    let bytes = uint8_array.to_vec();\n\n    let base = get_response_base(&response)?;\n\n    Ok(Response {\n        url: base.url,\n        ok: base.ok,\n        status: base.status,\n        status_text: base.status_text,\n        bytes,\n        headers: base.headers,\n    })\n}\n\n/// Spawn an async task.\n///\n/// A wrapper around `wasm_bindgen_futures::spawn_local`.\n/// Only available with the web backend.\npub fn spawn_future<F>(future: F)\nwhere\n    F: std::future::Future<Output = ()> + 'static,\n{\n    wasm_bindgen_futures::spawn_local(future);\n}\n\n// ----------------------------------------------------------------------------\n\npub(crate) fn fetch(request: Request, on_done: Box<dyn FnOnce(crate::Result<Response>) + Send>) {\n    spawn_future(async move {\n        let result = fetch_async(&request).await;\n        on_done(result)\n    });\n}\n"
  },
  {
    "path": "example_eframe/Cargo.toml",
    "content": "[package]\nname = \"example_eframe\"\nversion = \"0.1.0\"\nauthors = [\"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>\"]\ndescription = \"Demo of ehttp for both web and native using eframe\"\nedition = \"2018\"\nlicense = \"MIT OR Apache-2.0\"\npublish = false\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n[dependencies]\nehttp = { path = \"../ehttp\", features = [\"streaming\"] }\neframe = \"0.33.3\"\nlog = \"0.4.29\"\n\n# native:\n[target.'cfg(not(target_arch = \"wasm32\"))'.dependencies]\nenv_logger = \"0.11.4\"\n\n# web:\n[target.'cfg(target_arch = \"wasm32\")'.dependencies]\ngetrandom = { version = \"0.3.4\", features = [\"wasm_js\"] }\nwasm-bindgen-futures = \"0.4.45\"\nweb-sys = { version = \"0.3.85\", features = [\"HtmlCanvasElement\"] }\n"
  },
  {
    "path": "example_eframe/README.md",
    "content": "Example of using `ehttp` on web and native.\n\n## Native usage:\n```\ncargo run --release -p example_eframe\n```\n\n## Web usage:\n\n``` sh\n./setup_web.sh\n./start_server.sh &\n./build_web.sh --open\n```\n"
  },
  {
    "path": "example_eframe/build_web.sh",
    "content": "#!/bin/bash\nset -eu\nscript_path=$( cd \"$(dirname \"${BASH_SOURCE[0]}\")\" ; pwd -P )\ncd \"$script_path/..\"\n\n./example_eframe/setup_web.sh\n\nCRATE_NAME=\"example_eframe\"\n\nOPEN=false\nOPTIMIZE=false\nBUILD=debug\nBUILD_FLAGS=\"\"\nWASM_OPT_FLAGS=\"-O2 --fast-math\"\n\nwhile test $# -gt 0; do\n  case \"$1\" in\n    -h|--help)\n      echo \"build_demo_web.sh [--release] [--open]\"\n      echo \"  --open: open the result in a browser\"\n      echo \"  --release: Build with --release, and then run wasm-opt.\"\n      exit 0\n      ;;\n\n    --open)\n      shift\n      OPEN=true\n      ;;\n\n    --release)\n      shift\n      OPTIMIZE=true\n      BUILD=\"release\"\n      BUILD_FLAGS=\"--release\"\n      ;;\n\n    *)\n      break\n      ;;\n  esac\ndone\n\n# This is required to enable the web_sys clipboard API which egui_web uses\n# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html\n# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html\nexport RUSTFLAGS=\"--cfg=web_sys_unstable_apis --cfg getrandom_backend=\\\"wasm_js\\\"\"\n\nFINAL_WASM_PATH=web_demo/${CRATE_NAME}_bg.wasm\n\n# Clear output from old stuff:\nrm -f \"${FINAL_WASM_PATH}\"\n\necho \"Building rust…\"\n\ncargo build \\\n  -p ${CRATE_NAME} \\\n  ${BUILD_FLAGS} \\\n  --lib \\\n  --target wasm32-unknown-unknown\n\necho \"Generating JS bindings for wasm…\"\nTARGET_NAME=\"${CRATE_NAME}.wasm\"\nwasm-bindgen \"target/wasm32-unknown-unknown/$BUILD/$TARGET_NAME\" \\\n  --out-dir web_demo --no-modules --no-typescript\n\n# to get wasm-strip:  apt/brew/dnf install wabt\n# wasm-strip \"${FINAL_WASM_PATH}\"\n\nif [[ \"${OPTIMIZE}\" = true ]]; then\n  echo \"Optimizing wasm…\"\n  # to get wasm-opt:  apt/brew/dnf install binaryen\n  wasm-opt \"${FINAL_WASM_PATH}\" $WASM_OPT_FLAGS -o \"${FINAL_WASM_PATH}\"\nfi\n\necho \"Finished ${FINAL_WASM_PATH}\"\n\nif [ \"${OPEN}\" = true ]; then\n  if [[ \"$OSTYPE\" == \"linux-gnu\"* ]]; then\n    # Linux, ex: Fedora\n    xdg-open http://localhost:8787/index.html\n  elif [[ \"$OSTYPE\" == \"msys\" ]]; then\n    # Windows\n    start http://localhost:8787/index.html\n  else\n    # Darwin/MacOS, or something else\n    open http://localhost:8787/index.html\n  fi\nfi\n"
  },
  {
    "path": "example_eframe/setup_web.sh",
    "content": "#!/bin/bash\nset -eu\nscript_path=$( cd \"$(dirname \"${BASH_SOURCE[0]}\")\" ; pwd -P )\ncd \"$script_path/..\"\n\n# Pre-requisites:\nrustup target add wasm32-unknown-unknown\n\n# For generating JS bindings:\ncargo install --quiet wasm-bindgen-cli --version 0.2.108 --locked\n\n"
  },
  {
    "path": "example_eframe/src/app.rs",
    "content": "use std::{\n    ops::ControlFlow,\n    sync::{Arc, Mutex},\n};\n\nuse eframe::egui;\n\n#[derive(Debug, PartialEq, Copy, Clone)]\nenum Method {\n    Get,\n    Head,\n    Post,\n}\n\nenum Download {\n    None,\n    InProgress,\n    StreamingInProgress {\n        response: ehttp::PartialResponse,\n        body: Vec<u8>,\n    },\n    Done(ehttp::Result<ehttp::Response>),\n}\n\npub struct DemoApp {\n    url: String,\n    method: Method,\n    request_body: String,\n    streaming: bool,\n    download: Arc<Mutex<Download>>,\n}\n\nimpl Default for DemoApp {\n    fn default() -> Self {\n        Self {\n            url: \"https://raw.githubusercontent.com/emilk/ehttp/master/README.md\".to_owned(),\n            method: Method::Get,\n            request_body: r#\"[\"posting some json\"]\"#.to_owned(),\n            streaming: true,\n            download: Arc::new(Mutex::new(Download::None)),\n        }\n    }\n}\n\nimpl eframe::App for DemoApp {\n    fn update(&mut self, egui_ctx: &egui::Context, _frame: &mut eframe::Frame) {\n        egui::CentralPanel::default().show(egui_ctx, |ui| {\n            let trigger_fetch = self.ui_url(ui);\n\n            if trigger_fetch {\n                let request = match self.method {\n                    Method::Get => ehttp::Request::get(&self.url),\n                    Method::Head => ehttp::Request::head(&self.url),\n                    Method::Post => {\n                        ehttp::Request::post(&self.url, self.request_body.as_bytes().to_vec())\n                    }\n                };\n                let download_store = self.download.clone();\n                *download_store.lock().unwrap() = Download::InProgress;\n                let egui_ctx = egui_ctx.clone();\n\n                if self.streaming {\n                    // The more complicated streaming API:\n                    ehttp::streaming::fetch(request, move |part| {\n                        egui_ctx.request_repaint(); // Wake up UI thread\n                        on_fetch_part(part, &mut download_store.lock().unwrap())\n                    });\n                } else {\n                    // The simple non-streaming API:\n                    ehttp::fetch(request, move |response| {\n                        *download_store.lock().unwrap() = Download::Done(response);\n                        egui_ctx.request_repaint(); // Wake up UI thread\n                    });\n                }\n            }\n\n            ui.separator();\n\n            let download: &Download = &self.download.lock().unwrap();\n            match download {\n                Download::None => {}\n                Download::InProgress => {\n                    ui.label(\"Wait for it…\");\n                }\n                Download::StreamingInProgress { body, .. } => {\n                    let num_bytes = body.len();\n                    if num_bytes < 1_000_000 {\n                        ui.label(format!(\"{:.1} kB\", num_bytes as f32 / 1e3));\n                    } else {\n                        ui.label(format!(\"{:.1} MB\", num_bytes as f32 / 1e6));\n                    }\n                }\n                Download::Done(response) => match response {\n                    Err(err) => {\n                        ui.label(err);\n                    }\n                    Ok(response) => {\n                        response_ui(ui, response);\n                    }\n                },\n            }\n        });\n    }\n}\n\nfn on_fetch_part(\n    part: Result<ehttp::streaming::Part, String>,\n    download_store: &mut Download,\n) -> ControlFlow<()> {\n    let part = match part {\n        Err(err) => {\n            *download_store = Download::Done(Result::Err(err));\n            return ControlFlow::Break(());\n        }\n        Ok(part) => part,\n    };\n\n    match part {\n        ehttp::streaming::Part::Response(response) => {\n            *download_store = Download::StreamingInProgress {\n                response,\n                body: Vec::new(),\n            };\n            ControlFlow::Continue(())\n        }\n        ehttp::streaming::Part::Chunk(chunk) => {\n            if let Download::StreamingInProgress { response, mut body } =\n                std::mem::replace(download_store, Download::None)\n            {\n                body.extend_from_slice(&chunk);\n\n                if chunk.is_empty() {\n                    // This was the last chunk.\n                    *download_store = Download::Done(Ok(response.complete(body)));\n                    ControlFlow::Break(())\n                } else {\n                    // More to come.\n                    *download_store = Download::StreamingInProgress { response, body };\n                    ControlFlow::Continue(())\n                }\n            } else {\n                ControlFlow::Break(()) // some data race - abort download.\n            }\n        }\n    }\n}\n\nimpl DemoApp {\n    fn ui_url(&mut self, ui: &mut egui::Ui) -> bool {\n        let mut trigger_fetch = self.ui_examples(ui);\n\n        egui::Grid::new(\"request_parameters\")\n            .spacing(egui::Vec2::splat(4.0))\n            .min_col_width(70.0)\n            .num_columns(2)\n            .show(ui, |ui| {\n                ui.label(\"URL:\");\n                trigger_fetch |= ui.text_edit_singleline(&mut self.url).lost_focus();\n                ui.end_row();\n\n                ui.label(\"Method:\");\n                ui.horizontal(|ui| {\n                    ui.radio_value(&mut self.method, Method::Get, \"GET\")\n                        .clicked();\n                    ui.radio_value(&mut self.method, Method::Head, \"HEAD\")\n                        .clicked();\n                    ui.radio_value(&mut self.method, Method::Post, \"POST\")\n                        .clicked();\n                });\n                ui.end_row();\n\n                if self.method == Method::Post {\n                    ui.label(\"POST Body:\");\n                    ui.add(\n                        egui::TextEdit::multiline(&mut self.request_body)\n                            .code_editor()\n                            .desired_rows(1),\n                    );\n                    ui.end_row();\n                }\n\n                ui.checkbox(&mut self.streaming, \"Use streaming fetch\").on_hover_text(\n                    \"The ehttp::streaming API allows you to process the data piece by piece as it is received.\\n\\\n                    You might need to disable caching, throttle your download speed, and/or download a large file to see the data being streamed in.\");\n                ui.end_row();\n            });\n\n        trigger_fetch |= ui.button(\"fetch ▶\").clicked();\n\n        trigger_fetch\n    }\n\n    fn ui_examples(&mut self, ui: &mut egui::Ui) -> bool {\n        let mut trigger_fetch = false;\n\n        ui.horizontal(|ui| {\n            ui.label(\"Examples:\");\n\n            let self_url = format!(\n                \"https://raw.githubusercontent.com/emilk/ehttp/master/{}\",\n                file!()\n            );\n            if ui\n                .selectable_label(\n                    (&self.url, self.method) == (&self_url, Method::Get),\n                    \"GET source code\",\n                )\n                .clicked()\n            {\n                self.url = self_url;\n                self.method = Method::Get;\n                trigger_fetch = true;\n            }\n\n            let wasm_file = \"https://emilk.github.io/ehttp/example_eframe_bg.wasm\".to_owned();\n            if ui\n                .selectable_label(\n                    (&self.url, self.method) == (&wasm_file, Method::Get),\n                    \"GET .wasm\",\n                )\n                .clicked()\n            {\n                self.url = wasm_file;\n                self.method = Method::Get;\n                trigger_fetch = true;\n            }\n\n            let pastebin_url = \"https://httpbin.org/post\".to_owned();\n            if ui\n                .selectable_label(\n                    (&self.url, self.method) == (&pastebin_url, Method::Post),\n                    \"POST to httpbin.org\",\n                )\n                .clicked()\n            {\n                self.url = pastebin_url;\n                self.method = Method::Post;\n                trigger_fetch = true;\n            }\n        });\n\n        trigger_fetch\n    }\n}\n\nfn response_ui(ui: &mut egui::Ui, response: &ehttp::Response) {\n    ui.monospace(format!(\"url:          {}\", response.url));\n    ui.monospace(format!(\n        \"status:       {} ({})\",\n        response.status, response.status_text\n    ));\n    ui.monospace(format!(\n        \"content-type: {}\",\n        response.content_type().unwrap_or_default()\n    ));\n    ui.monospace(format!(\n        \"size:         {:.1} kB\",\n        response.bytes.len() as f32 / 1000.0\n    ));\n\n    ui.separator();\n\n    egui::ScrollArea::vertical().show(ui, |ui| {\n        egui::CollapsingHeader::new(\"Response headers\")\n            .default_open(false)\n            .show(ui, |ui| {\n                egui::Grid::new(\"response_headers\")\n                    .spacing(egui::vec2(ui.spacing().item_spacing.x * 2.0, 0.0))\n                    .show(ui, |ui| {\n                        for (k, v) in &response.headers {\n                            ui.label(k);\n                            ui.label(v);\n                            ui.end_row();\n                        }\n                    })\n            });\n\n        ui.separator();\n\n        if let Some(text) = response.text() {\n            let tooltip = \"Click to copy the response body\";\n            if ui.button(\"📋\").on_hover_text(tooltip).clicked() {\n                ui.ctx().copy_text(text.to_owned());\n            }\n            ui.separator();\n        }\n\n        if let Some(text) = response.text() {\n            selectable_text(ui, text);\n        } else {\n            ui.monospace(\"[binary]\");\n        }\n    });\n}\n\nfn selectable_text(ui: &mut egui::Ui, mut text: &str) {\n    ui.add(\n        egui::TextEdit::multiline(&mut text)\n            .desired_width(f32::INFINITY)\n            .font(egui::TextStyle::Monospace.resolve(ui.style())),\n    );\n}\n"
  },
  {
    "path": "example_eframe/src/lib.rs",
    "content": "//! Example application using [`eframe`].\n\nmod app;\n\npub use app::DemoApp;\n\n// ----------------------------------------------------------------------------\n\n#[cfg(target_arch = \"wasm32\")]\nmod web;\n\n#[cfg(target_arch = \"wasm32\")]\npub use web::*;\n"
  },
  {
    "path": "example_eframe/src/main.rs",
    "content": "fn main() -> eframe::Result<()> {\n    env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).\n\n    eframe::run_native(\n        \"ehttp demo\",\n        Default::default(),\n        Box::new(|_cc| Ok(Box::<example_eframe::DemoApp>::default())),\n    )\n}\n"
  },
  {
    "path": "example_eframe/src/web.rs",
    "content": "use eframe::wasm_bindgen::{self, prelude::*};\n\nuse crate::DemoApp;\n\n/// Call this once from JavaScript to start your app.\n#[wasm_bindgen]\npub async fn start(canvas: web_sys::HtmlCanvasElement) -> Result<(), wasm_bindgen::JsValue> {\n    eframe::WebLogger::init(log::LevelFilter::Debug).ok();\n\n    eframe::WebRunner::new()\n        .start(\n            canvas,\n            eframe::WebOptions::default(),\n            Box::new(|_cc| Ok(Box::<DemoApp>::default())),\n        )\n        .await\n}\n"
  },
  {
    "path": "example_eframe/start_server.sh",
    "content": "#!/bin/bash\nset -eu\nscript_path=$( cd \"$(dirname \"${BASH_SOURCE[0]}\")\" ; pwd -P )\ncd \"$script_path/..\"\n\n# Starts a local web-server that serves the contents of the `doc/` folder,\n\necho \"ensuring basic-http-server is installed…\"\ncargo install basic-http-server\n\necho \"starting server…\"\necho \"serving at http://localhost:8787\"\n\n(cd web_demo && basic-http-server --addr 127.0.0.1:8787 .)\n# (cd web_demo && python3 -m http.server 8787 --bind 127.0.0.1)\n"
  },
  {
    "path": "rust-toolchain",
    "content": "# If you see this, run \"rustup self update\" to get rustup 1.23 or newer.\n\n# NOTE: above comment is for older `rustup` (before TOML support was added),\n# which will treat the first line as the toolchain name, and therefore show it\n# to the user in the error, instead of \"error: invalid channel name '[toolchain]'\".\n\n[toolchain]\nchannel = \"1.88.0\"\ncomponents = [ \"rustfmt\", \"clippy\" ]\ntargets = [ \"wasm32-unknown-unknown\" ]\n"
  },
  {
    "path": "sh/check.sh",
    "content": "#!/bin/bash\nscript_path=$( cd \"$(dirname \"${BASH_SOURCE[0]}\")\" ; pwd -P )\ncd \"$script_path/..\"\nset -eux\n\n# Checks all tests, lints etc.\n# Basically does what the CI does.\n\ncargo check --workspace --all-targets --all-features\ncargo test --workspace --doc\ncargo check --lib --target wasm32-unknown-unknown --all-features\ncargo clippy --workspace --all-targets --all-features --  -D warnings -W clippy::all\ncargo test --workspace --all-targets --all-features\ncargo fmt --all -- --check\n\ncargo doc --lib --no-deps --all-features\ncargo doc --target wasm32-unknown-unknown --lib --no-deps --all-features\n\ncargo deny check\n"
  },
  {
    "path": "sh/docs.sh",
    "content": "#!/bin/bash\nset -eu\nscript_path=$( cd \"$(dirname \"${BASH_SOURCE[0]}\")\" ; pwd -P )\ncd \"$script_path/..\"\n\ncargo doc -p ehttp --lib --no-deps --all-features --open\n\n# cargo watch -c -x 'doc -p ehttp --lib --no-deps --all-features'\n"
  },
  {
    "path": "web_demo/.gitignore",
    "content": "example_eframe_bg.wasm\nexample_eframe.js\n"
  },
  {
    "path": "web_demo/README.md",
    "content": "Build the web demo using `./example_eframe/build_web.sh` and serve it with `./example_eframe/start_server.sh`.\n"
  },
  {
    "path": "web_demo/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n\n<!-- Disable zooming: -->\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n\n<head>\n    <title>ehttp example</title>\n    <style>\n        html {\n            /* Remove touch delay: */\n            touch-action: manipulation;\n        }\n\n        body {\n            /* Light mode background color for what is not covered by the egui canvas,\n                or where the egui canvas is translucent. */\n            background: #909090;\n        }\n\n        @media (prefers-color-scheme: dark) {\n            body {\n                /* Dark mode background color for what is not covered by the egui canvas,\n                    or where the egui canvas is translucent. */\n                background: #404040;\n            }\n        }\n\n        /* Allow canvas to fill entire web page: */\n        html,\n        body {\n            overflow: hidden;\n            margin: 0 !important;\n            padding: 0 !important;\n            height: 100%;\n            width: 100%;\n        }\n\n        canvas {\n            margin-right: auto;\n            margin-left: auto;\n            display: block;\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n        }\n\n        .centered {\n            margin-right: auto;\n            margin-left: auto;\n            display: block;\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            color: #f0f0f0;\n            font-size: 24px;\n            font-family: Ubuntu-Light, Helvetica, sans-serif;\n            text-align: center;\n        }\n\n        /* ---------------------------------------------- */\n        /* Loading animation from https://loading.io/css/ */\n        .lds-dual-ring {\n            display: inline-block;\n            width: 24px;\n            height: 24px;\n        }\n\n        .lds-dual-ring:after {\n            content: \" \";\n            display: block;\n            width: 24px;\n            height: 24px;\n            margin: 0px;\n            border-radius: 50%;\n            border: 3px solid #fff;\n            border-color: #fff transparent #fff transparent;\n            animation: lds-dual-ring 1.2s linear infinite;\n        }\n\n        @keyframes lds-dual-ring {\n            0% {\n                transform: rotate(0deg);\n            }\n\n            100% {\n                transform: rotate(360deg);\n            }\n        }\n    </style>\n</head>\n\n<body>\n    <canvas id=\"the_canvas_id\"></canvas>\n    <div class=\"centered\" id=\"loading_text\">\n        <noscript>You need javascript to use this website</noscript>\n        <p style=\"font-size:16px\">Loading…</p>\n        <div class=\"lds-dual-ring\"></div>\n    </div>\n\n    <script>\n        // The `--no-modules`-generated JS from `wasm-bindgen` attempts to use\n        // `WebAssembly.instantiateStreaming` to instantiate the wasm module,\n        // but this doesn't work with `file://` urls. This example is frequently\n        // viewed by simply opening `index.html` in a browser (with a `file://`\n        // url), so it would fail if we were to call this function!\n        //\n        // Work around this for now by deleting the function to ensure that the\n        // `no_modules.js` script doesn't have access to it. You won't need this\n        // hack when deploying over HTTP.\n        delete WebAssembly.instantiateStreaming;\n    </script>\n\n    <!-- this is the JS generated by the `wasm-bindgen` CLI tool -->\n    <script src=\"example_eframe.js\"></script>\n\n    <script>\n        // We'll defer our execution until the wasm is ready to go.\n        // Here we tell bindgen the path to the wasm file so it can start\n        // initialization and return to us a promise when it's done.\n        console.debug(\"Loading wasm…\");\n        wasm_bindgen(\"./example_eframe_bg.wasm\")\n            .then(on_wasm_loaded)\n            .catch(console.error);\n\n        function on_wasm_loaded() {\n            console.debug(\"Wasm loaded. Starting app…\");\n\n            wasm_bindgen.start(document.getElementById(\"the_canvas_id\")).then(on_app_started).catch(on_error);\n        }\n\n        function on_app_started(handle) {\n            // Call `handle.destroy()` to stop. Uncomment to quick result:\n            // setTimeout(() => { handle.destroy(); handle.free()) }, 2000)\n\n            console.debug(\"App started.\");\n            document.getElementById(\"loading_text\").innerHTML = '';\n        }\n\n        function on_error(error) {\n            console.error(\"Failed to start: \" + error);\n            document.getElementById(\"the_canvas_id\").remove();\n            document.getElementById(\"loading_text\").innerHTML = `\n                <p>\n                    An error occurred during loading:\n                </p>\n                <p style=\"font-family:Courier New\">\n                    ${error}\n                </p>\n                <p style=\"font-size:14px\">\n                    Make sure you use a modern browser with WebGL and WASM enabled.\n                </p>`;\n        }\n    </script>\n</body>\n\n</html>\n\n<!-- Powered by egui: https://github.com/emilk/egui/ -->\n"
  }
]