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

Loading…