1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-07 19:23:58 +02:00

Compare commits

..

41 Commits

Author SHA1 Message Date
eb10b74751 Merge branch 'master' into error-response-mapping 2024-06-10 01:31:51 +01:00
a2b9823d9d Strip non-address characters from Forwarded for= (#3343)
* Strip non-address characters from Forwarded for=

This is something of a followup to #2528, which asked for port information to not be included in  when it was taken from the local socket.

The  header's  element may optionally contain port information (https://datatracker.ietf.org/doc/html/rfc7239#section-6).
However, as I understand it,  is *supposed* to only contain an IP address, without port (per #2528).

This PR corrects that discrepancy, making it easier to parse the result of this method in application code.

There should not be any compatibility concerns, as anyone parsing the output of  would already need to handle both port and portless cases anyway.

* Update CHANGES.md

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-09 23:40:09 +00:00
da56de4556 chore(actix-test): prepare release 0.1.5 2024-06-10 00:01:17 +01:00
758ae1dac1 actix-test: allow the configuration of the TestServer address (#3351)
* actix-test: allow the configuration of the TestServer address

This is useful if you're running (say) Selenium tests against a running TestServer, and the Selenium workers are Docker containers elsewhere in the network.
Not a *particularly* common use case, perhaps, but one that I can attest happens every now and then.

* Update CHANGES.md

* Adjust default listen address to avoid test failures

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-09 19:07:08 +00:00
37577dcb89 chore(actix-files): prepare release 0.6.6 2024-06-09 19:45:14 +01:00
8b8eb4eae1 build(deps): update tokio-uring requirement from 0.4 to 0.5 (#3385)
* build(deps): update tokio-uring requirement from 0.4 to 0.5

Updates the requirements on [tokio-uring](https://github.com/tokio-rs/tokio-uring) to permit the latest version.
- [Release notes](https://github.com/tokio-rs/tokio-uring/releases)
- [Changelog](https://github.com/tokio-rs/tokio-uring/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/tokio-uring/compare/v0.4.0...v0.5.0)

---
updated-dependencies:
- dependency-name: tokio-uring
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: narrow actix-server requirement

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-09 17:37:42 +00:00
22593a1532 Re-export http::status::InvalidStatusCode (#3393)
* [actix-http/src/lib.rs] Expose/re-export `http::status::InvalidStatusCode`

* [actix-http/src/error.rs] Re-export `http::status::InvalidStatusCode` ; [actix-http/src/lib.rs] Revert
2024-06-09 05:07:56 +00:00
f7646bcc48 actix-web-actors: take the internal buffer when yielding (#3369)
* actix-web-actors: take the internal buffer when yielding

* actix-web-actors: Add CHANGES entry re: taking buffer
2024-06-09 05:04:42 +00:00
8018983a68 docs: update changelog for #3393 2024-06-09 06:08:21 +01:00
266834cf7c chore: narrow h2 version 2024-06-09 04:51:53 +01:00
40e1034566 docs: update changelog 2024-06-09 00:38:49 +01:00
a5c78483f9 chore(actix-web): prepare release 4.7.0 2024-06-09 00:22:03 +01:00
12a0521ef8 chore(actix-multipart): prepare release 0.6.2 2024-06-09 00:20:36 +01:00
b4faf8820c chore(actix-web-codegen): prepare release 4.3.0 2024-06-09 00:19:09 +01:00
d6f885127d chore(actix-test): prepare release 0.1.4 2024-06-09 00:16:36 +01:00
ebc43dcf1b feat: forwards-compatibility for handler visibility inheritance fix (#3391) 2024-06-09 00:10:15 +01:00
7c4c26d2df feat: expose Identity middleware (#3390) 2024-06-08 05:26:26 +01:00
3db7891303 Scope macro (#3136)
* add scope proc macro

* Update scope macro code to work with current HttpServiceFactory

* started some test code

* add some unit tests

* code formatting cleanup

* add another test for combining and calling 2 scopes

* format code with formatter

* Update actix-web-codegen/src/lib.rs with comment documentation fix

Co-authored-by: oliver <151407407+kwfn@users.noreply.github.com>

* work in progress. revised procedural macro to change othe macro call

* add tests again. refactor nested code.

* clean up code. fix bugs with route and method attributes with parameters

* clean up for rust fmt

* clean up for rust fmt

* fix out of date comment for scope macro

* sync to master branch by adding test_wrap

* needed to format code

* test: split out scope tests

* test: add negative tests

* chore: move imports back inside (?)

* docs: tweak scope docs

* fix: prevent trailing slashes in scope prefixes

* chore: address clippy lints

---------

Co-authored-by: oliver <151407407+kwfn@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-07 22:10:48 +00:00
c366649516 docs: example of CPU core pinning 2024-06-07 16:57:13 +01:00
534cfe1fda feat: add .customize().add_cookie() (#3215)
* feat: add .customize().add_cookie()

* docs: added cookie hint

* fix: added unwrap to test of add_cookie()

* docs: added changelog entry for .customize().add_cookie()

* chore: make append_header infallible

* docs: update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-07 15:22:48 +00:00
cff958e518 chore: address clippy lint 2024-06-07 16:10:25 +01:00
b9305ff59d chore: fmt 2024-06-07 16:05:42 +01:00
5221c1b194 ci: pin msrv lookup job 2024-06-07 16:05:41 +01:00
4493aa35d0 actix-http::ws: Remove redundant + 4 byte reservation when masked (#3371)
* actix-http::ws: Remove redundant + 4 byte reservation when masked

* actix-http: Update CHANGES wrt byte fix

* docs: remove changelog entry

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-07 14:41:32 +00:00
8b4d23a69a Allow disabling redirect following in actix-test (#3356)
If you're testing that redirects are being properly generated, then it's
useful to not have the client go off on a wild goose chase of its own.

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-07 14:40:55 +00:00
8fdf358954 Add app_data method to GuardContext (#3341)
* changes: guard

* fix(guard): docs link to app_data

* docs: fix changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-07 14:31:53 +00:00
b2d0196f34 Do not require actix-router default features from actix-web-codegen (#3372)
* fix: Do not require actix-router default features from actix-web-codegen

* docs: update changelog

* test: update trybuild stderr

---------

Co-authored-by: Dylan Anthony <dbanty@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-07 14:08:13 +00:00
85655f731d From Boxed ResponseError impl added (#3388)
* From Boxed ResponseError impl added

* docs: update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-07 13:55:29 +00:00
ebd8bb266d ci: fix cargo-public-api 2024-06-07 14:31:20 +01:00
5c18569b78 docs: align App:app_data arg name 2024-06-07 14:31:20 +01:00
dd84bcb609 build(deps): bump taiki-e/install-action from 2.33.34 to 2.34.0 (#3386)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.33.34 to 2.34.0.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.33.34...v2.34.0)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 01:24:09 +00:00
3ce97effa2 ci: delete upload doc workflow 2024-05-28 01:21:23 +01:00
26efa64278 build(deps): bump taiki-e/install-action from 2.33.26 to 2.33.34 (#3380)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.33.26 to 2.33.34.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.33.26...v2.33.34)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 00:58:18 +00:00
cc06fd6a5e build(deps): bump codecov/codecov-action from 4.4.0 to 4.4.1 (#3381)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.4.0 to 4.4.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.4.0...v4.4.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 00:57:59 +00:00
1b214bc5f5 build(deps): bump codecov/codecov-action from 4.3.1 to 4.4.0 (#3374)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.3.1 to 4.4.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.3.1...v4.4.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-20 09:30:48 +00:00
d4bcdf28f2 build(deps): bump JamesIves/github-pages-deploy-action from 4.6.0 to 4.6.1 (#3375)
build(deps): bump JamesIves/github-pages-deploy-action

Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.6.0...v4.6.1)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-20 09:30:27 +00:00
4f7b334d80 build(deps): bump taiki-e/install-action from 2.33.22 to 2.33.26 (#3376)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.33.22 to 2.33.26.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.33.22...v2.33.26)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-20 09:27:44 +00:00
fdff3775a8 ci: use mold linker (#3370) 2024-05-19 20:24:33 +01:00
b342b8fc82 chore(actix-router): prepare release 0.5.3 2024-05-19 12:09:46 +01:00
804a344565 ci: limit cargo hack concurrency 2024-05-19 12:06:20 +01:00
98c99f3bc2 add methods for mapping error responses 2023-02-13 23:40:23 +00:00
59 changed files with 771 additions and 157 deletions

View File

@ -6,5 +6,5 @@ lint-all = "clippy --workspace --all-features --all-targets -- -Dclippy::todo"
ci-check-min = "hack --workspace check --no-default-features" ci-check-min = "hack --workspace check --no-default-features"
ci-check-default = "hack --workspace check" ci-check-default = "hack --workspace check"
ci-check-default-tests = "check --workspace --tests" ci-check-default-tests = "check --workspace --tests"
ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,experimental-io-uring check" ci-check-all-feature-powerset="hack --workspace --feature-powerset --depth=4 --skip=__compress,experimental-io-uring check"
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check" ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --depth=4 --skip=__compress check"

View File

@ -49,7 +49,7 @@ jobs:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.33.22 uses: taiki-e/install-action@v2.34.0
with: with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -80,7 +80,7 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
- name: Install cargo-hack - name: Install cargo-hack
uses: taiki-e/install-action@v2.33.22 uses: taiki-e/install-action@v2.34.0
with: with:
tool: cargo-hack tool: cargo-hack

View File

@ -18,7 +18,7 @@ concurrency:
jobs: jobs:
read_msrv: read_msrv:
name: Read MSRV name: Read MSRV
uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@main uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@v0.1.0
build_and_test: build_and_test:
needs: read_msrv needs: read_msrv
@ -54,13 +54,17 @@ jobs:
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
- name: Setup mold linker
if: matrix.target.os == 'ubuntu-latest'
uses: rui314/setup-mold@v1
- name: Install Rust (${{ matrix.version.name }}) - name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.33.22 uses: taiki-e/install-action@v2.34.0
with: with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -109,7 +113,7 @@ jobs:
toolchain: nightly toolchain: nightly
- name: Install just - name: Install just
uses: taiki-e/install-action@v2.33.22 uses: taiki-e/install-action@v2.34.0
with: with:
tool: just tool: just

View File

@ -22,16 +22,16 @@ jobs:
with: with:
components: llvm-tools-preview components: llvm-tools-preview
- name: Install cargo-llvm-cov - name: Install just,cargo-llvm-cov
uses: taiki-e/install-action@v2.33.22 uses: taiki-e/install-action@v2.34.0
with: with:
tool: cargo-llvm-cov tool: just,cargo-llvm-cov
- name: Generate code coverage - name: Generate code coverage
run: cargo llvm-cov --workspace --all-features --codecov --output-path codecov.json run: cargo llvm-cov --workspace --all-features --codecov --output-path codecov.json
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4.3.1 uses: codecov/codecov-action@v4.4.1
with: with:
files: codecov.json files: codecov.json
fail_ci_if_error: true fail_ci_if_error: true

View File

@ -79,15 +79,15 @@ jobs:
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: with:
toolchain: nightly-2024-04-26 toolchain: nightly-2024-06-07
- name: Install cargo-public-api - name: Install cargo-public-api
uses: taiki-e/install-action@v2.33.22 uses: taiki-e/install-action@v2.34.0
with: with:
tool: cargo-public-api tool: cargo-public-api
- name: Generate API diff - name: Generate API diff
run: | run: |
for f in $(find -mindepth 2 -maxdepth 2 -name Cargo.toml); do for f in $(find -mindepth 2 -maxdepth 2 -name Cargo.toml); do
cargo public-api --manifest-path "$f" diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }} cargo public-api --manifest-path "$f" --simplified diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }}
done done

View File

@ -1,41 +0,0 @@
name: Upload Documentation
on:
push:
branches: [master]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with:
toolchain: nightly
- name: Build Docs
run: cargo +nightly doc --no-deps --workspace --all-features
env:
RUSTDOCFLAGS: --cfg=docsrs
- name: Tweak HTML
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4.6.0
with:
folder: target/doc
single-commit: true

View File

@ -2,6 +2,9 @@
## Unreleased ## Unreleased
## 0.6.6
- Update `tokio-uring` dependency to `0.4`.
- Minimum supported Rust version (MSRV) is now 1.72. - Minimum supported Rust version (MSRV) is now 1.72.
## 0.6.5 ## 0.6.5

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.5" version = "0.6.6"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -40,8 +40,8 @@ v_htmlescape = "0.15.5"
# experimental-io-uring # experimental-io-uring
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
tokio-uring = { version = "0.4", optional = true, features = ["bytes"] } tokio-uring = { version = "0.5", optional = true, features = ["bytes"] }
actix-server = { version = "2.2", optional = true } # ensure matching tokio-uring versions actix-server = { version = "2.4", optional = true } # ensure matching tokio-uring versions
[dev-dependencies] [dev-dependencies]
actix-rt = "2.7" actix-rt = "2.7"

View File

@ -3,11 +3,11 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.5)](https://docs.rs/actix-files/0.6.5) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.6)](https://docs.rs/actix-files/0.6.6)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-files.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.5/status.svg)](https://deps.rs/crate/actix-files/0.6.5) [![dependency status](https://deps.rs/crate/actix-files/0.6.6/status.svg)](https://deps.rs/crate/actix-files/0.6.6)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -2,6 +2,10 @@
## Unreleased ## Unreleased
### Added
- Add `error::InvalidStatusCode` re-export.
## 3.7.0 ## 3.7.0
### Added ### Added

View File

@ -106,7 +106,7 @@ tokio-util = { version = "0.7", features = ["io", "codec"] }
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }
# http2 # http2
h2 = { version = "0.3.24", optional = true } h2 = { version = "0.3.26", optional = true }
# websockets # websockets
local-channel = { version = "0.1", optional = true } local-channel = { version = "0.1", optional = true }

View File

@ -3,7 +3,7 @@
use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Error}; use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Error};
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
pub use http::Error as HttpError; pub use http::{status::InvalidStatusCode, Error as HttpError};
use http::{uri::InvalidUri, StatusCode}; use http::{uri::InvalidUri, StatusCode};
use crate::{body::BoxBody, Response}; use crate::{body::BoxBody, Response};

View File

@ -178,14 +178,14 @@ impl Parser {
}; };
if payload_len < 126 { if payload_len < 126 {
dst.reserve(p_len + 2 + if mask { 4 } else { 0 }); dst.reserve(p_len + 2);
dst.put_slice(&[one, two | payload_len as u8]); dst.put_slice(&[one, two | payload_len as u8]);
} else if payload_len <= 65_535 { } else if payload_len <= 65_535 {
dst.reserve(p_len + 4 + if mask { 4 } else { 0 }); dst.reserve(p_len + 4);
dst.put_slice(&[one, two | 126]); dst.put_slice(&[one, two | 126]);
dst.put_u16(payload_len as u16); dst.put_u16(payload_len as u16);
} else { } else {
dst.reserve(p_len + 10 + if mask { 4 } else { 0 }); dst.reserve(p_len + 10);
dst.put_slice(&[one, two | 127]); dst.put_slice(&[one, two | 127]);
dst.put_u64(payload_len as u64); dst.put_u64(payload_len as u64);
}; };

View File

@ -2,6 +2,8 @@
## Unreleased ## Unreleased
## 0.6.2
- Add testing utilities under new module `test`. - Add testing utilities under new module `test`.
- Minimum supported Rust version (MSRV) is now 1.72. - Minimum supported Rust version (MSRV) is now 1.72.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.6.1" version = "0.6.2"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Jacob Halsey <jacob@jhalsey.com>", "Jacob Halsey <jacob@jhalsey.com>",

View File

@ -5,17 +5,16 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart)
[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.6.1)](https://docs.rs/actix-multipart/0.6.1) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.6.2)](https://docs.rs/actix-multipart/0.6.2)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-multipart/0.6.1/status.svg)](https://deps.rs/crate/actix-multipart/0.6.1) [![dependency status](https://deps.rs/crate/actix-multipart/0.6.2/status.svg)](https://deps.rs/crate/actix-multipart/0.6.2)
[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
## Example ## Example
Dependencies: Dependencies:
@ -65,6 +64,7 @@ async fn main() -> std::io::Result<()> {
``` ```
Curl request : Curl request :
```bash ```bash
curl -v --request POST \ curl -v --request POST \
--url http://localhost:8080/videos \ --url http://localhost:8080/videos \
@ -72,7 +72,6 @@ curl -v --request POST \
-F file=@./Cargo.lock -F file=@./Cargo.lock
``` ```
### Examples ### Examples
https://github.com/actix/examples/tree/master/forms/multipart https://github.com/actix/examples/tree/master/forms/multipart

View File

@ -2,6 +2,8 @@
## Unreleased ## Unreleased
## 0.5.3
- Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size. - Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size.
- Minimum supported Rust version (MSRV) is now 1.72. - Minimum supported Rust version (MSRV) is now 1.72.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-router" name = "actix-router"
version = "0.5.2" version = "0.5.3"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>", "Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",

View File

@ -3,11 +3,11 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-router?label=latest)](https://crates.io/crates/actix-router) [![crates.io](https://img.shields.io/crates/v/actix-router?label=latest)](https://crates.io/crates/actix-router)
[![Documentation](https://docs.rs/actix-router/badge.svg?version=0.5.2)](https://docs.rs/actix-router/0.5.2) [![Documentation](https://docs.rs/actix-router/badge.svg?version=0.5.3)](https://docs.rs/actix-router/0.5.3)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-router.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-router.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-router/0.5.2/status.svg)](https://deps.rs/crate/actix-router/0.5.2) [![dependency status](https://deps.rs/crate/actix-router/0.5.3/status.svg)](https://deps.rs/crate/actix-router/0.5.3)
[![Download](https://img.shields.io/crates/d/actix-router.svg)](https://crates.io/crates/actix-router) [![Download](https://img.shields.io/crates/d/actix-router.svg)](https://crates.io/crates/actix-router)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -2,9 +2,16 @@
## Unreleased ## Unreleased
## 0.1.5
- Add `TestServerConfig::listen_address()` method.
## 0.1.4
- Add `TestServerConfig::rustls_0_23()` method for Rustls v0.23 support behind new `rustls-0_23` crate feature. - Add `TestServerConfig::rustls_0_23()` method for Rustls v0.23 support behind new `rustls-0_23` crate feature.
- Minimum supported Rust version (MSRV) is now 1.72. - Add `TestServerConfig::disable_redirects()` method.
- Various types from `awc`, such as `ClientRequest` and `ClientResponse`, are now re-exported. - Various types from `awc`, such as `ClientRequest` and `ClientResponse`, are now re-exported.
- Minimum supported Rust version (MSRV) is now 1.72.
## 0.1.3 ## 0.1.3

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-test" name = "actix-test"
version = "0.1.3" version = "0.1.5"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",

View File

@ -149,10 +149,12 @@ where
StreamType::Rustls023(_) => true, StreamType::Rustls023(_) => true,
}; };
let client_cfg = cfg.clone();
// run server in separate orphaned thread // run server in separate orphaned thread
thread::spawn(move || { thread::spawn(move || {
rt::System::new().block_on(async move { rt::System::new().block_on(async move {
let tcp = net::TcpListener::bind(("127.0.0.1", cfg.port)).unwrap(); let tcp = net::TcpListener::bind((cfg.listen_address.clone(), cfg.port)).unwrap();
let local_addr = tcp.local_addr().unwrap(); let local_addr = tcp.local_addr().unwrap();
let factory = factory.clone(); let factory = factory.clone();
let srv_cfg = cfg.clone(); let srv_cfg = cfg.clone();
@ -460,7 +462,13 @@ where
} }
}; };
Client::builder().connector(connector).finish() let mut client_builder = Client::builder().connector(connector);
if client_cfg.disable_redirects {
client_builder = client_builder.disable_redirects();
}
client_builder.finish()
}; };
TestServer { TestServer {
@ -480,6 +488,7 @@ enum HttpVer {
Both, Both,
} }
#[allow(clippy::large_enum_variant)]
#[derive(Clone)] #[derive(Clone)]
enum StreamType { enum StreamType {
Tcp, Tcp,
@ -505,8 +514,10 @@ pub struct TestServerConfig {
tp: HttpVer, tp: HttpVer,
stream: StreamType, stream: StreamType,
client_request_timeout: Duration, client_request_timeout: Duration,
listen_address: String,
port: u16, port: u16,
workers: usize, workers: usize,
disable_redirects: bool,
} }
impl Default for TestServerConfig { impl Default for TestServerConfig {
@ -522,8 +533,10 @@ impl TestServerConfig {
tp: HttpVer::Both, tp: HttpVer::Both,
stream: StreamType::Tcp, stream: StreamType::Tcp,
client_request_timeout: Duration::from_secs(5), client_request_timeout: Duration::from_secs(5),
listen_address: "127.0.0.1".to_string(),
port: 0, port: 0,
workers: 1, workers: 1,
disable_redirects: false,
} }
} }
@ -596,6 +609,14 @@ impl TestServerConfig {
self self
} }
/// Sets the address the server will listen on.
///
/// By default, only listens on `127.0.0.1`.
pub fn listen_address(mut self, addr: impl Into<String>) -> Self {
self.listen_address = addr.into();
self
}
/// Sets test server port. /// Sets test server port.
/// ///
/// By default, a random free port is determined by the OS. /// By default, a random free port is determined by the OS.
@ -611,6 +632,15 @@ impl TestServerConfig {
self.workers = workers; self.workers = workers;
self self
} }
/// Instruct the client to not follow redirects.
///
/// By default, the client will follow up to 10 consecutive redirects
/// before giving up.
pub fn disable_redirects(mut self) -> Self {
self.disable_redirects = true;
self
}
} }
/// A basic HTTP server controller that simplifies the process of writing integration tests for /// A basic HTTP server controller that simplifies the process of writing integration tests for
@ -637,9 +667,9 @@ impl TestServer {
let scheme = if self.tls { "https" } else { "http" }; let scheme = if self.tls { "https" } else { "http" };
if uri.starts_with('/') { if uri.starts_with('/') {
format!("{}://localhost:{}{}", scheme, self.addr.port(), uri) format!("{}://{}{}", scheme, self.addr, uri)
} else { } else {
format!("{}://localhost:{}/{}", scheme, self.addr.port(), uri) format!("{}://{}/{}", scheme, self.addr, uri)
} }
} }

View File

@ -2,6 +2,7 @@
## Unreleased ## Unreleased
- Take the encoded buffer when yielding bytes in the response stream rather than splitting the buffer, reducing memory use
- Minimum supported Rust version (MSRV) is now 1.72. - Minimum supported Rust version (MSRV) is now 1.72.
## 4.3.0 ## 4.3.0

View File

@ -710,7 +710,7 @@ where
} }
if !this.buf.is_empty() { if !this.buf.is_empty() {
Poll::Ready(Some(Ok(this.buf.split().freeze()))) Poll::Ready(Some(Ok(std::mem::take(&mut this.buf).freeze())))
} else if this.fut.alive() && !this.closed { } else if this.fut.alive() && !this.closed {
Poll::Pending Poll::Pending
} else { } else {

View File

@ -2,6 +2,11 @@
## Unreleased ## Unreleased
## 4.3.0
- Add `#[scope]` macro.
- Add `compat-routing-macros-force-pub` crate feature which, on-by-default, which when disabled causes handlers to inherit their attached function's visibility.
- Prevent inclusion of default `actix-router` features.
- Minimum supported Rust version (MSRV) is now 1.72. - Minimum supported Rust version (MSRV) is now 1.72.
## 4.2.2 ## 4.2.2

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-codegen" name = "actix-web-codegen"
version = "4.2.2" version = "4.3.0"
description = "Routing and runtime macros for Actix Web" description = "Routing and runtime macros for Actix Web"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
@ -15,8 +15,12 @@ rust-version.workspace = true
[lib] [lib]
proc-macro = true proc-macro = true
[features]
default = ["compat-routing-macros-force-pub"]
compat-routing-macros-force-pub = []
[dependencies] [dependencies]
actix-router = "0.5" actix-router = { version = "0.5", default-features = false }
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "2", features = ["full", "extra-traits"] } syn = { version = "2", features = ["full", "extra-traits"] }

View File

@ -5,11 +5,11 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.2.2)](https://docs.rs/actix-web-codegen/4.2.2) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.3.0)](https://docs.rs/actix-web-codegen/4.3.0)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-web-codegen.svg) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-codegen/4.2.2/status.svg)](https://deps.rs/crate/actix-web-codegen/4.2.2) [![dependency status](https://deps.rs/crate/actix-web-codegen/4.3.0/status.svg)](https://deps.rs/crate/actix-web-codegen/4.3.0)
[![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -83,6 +83,7 @@ use proc_macro::TokenStream;
use quote::quote; use quote::quote;
mod route; mod route;
mod scope;
/// Creates resource handler, allowing multiple HTTP method guards. /// Creates resource handler, allowing multiple HTTP method guards.
/// ///
@ -197,6 +198,43 @@ method_macro!(Options, options);
method_macro!(Trace, trace); method_macro!(Trace, trace);
method_macro!(Patch, patch); method_macro!(Patch, patch);
/// Prepends a path prefix to all handlers using routing macros inside the attached module.
///
/// # Syntax
///
/// ```
/// # use actix_web_codegen::scope;
/// #[scope("/prefix")]
/// mod api {
/// // ...
/// }
/// ```
///
/// # Arguments
///
/// - `"/prefix"` - Raw literal string to be prefixed onto contained handlers' paths.
///
/// # Example
///
/// ```
/// # use actix_web_codegen::{scope, get};
/// # use actix_web::Responder;
/// #[scope("/api")]
/// mod api {
/// # use super::*;
/// #[get("/hello")]
/// pub async fn hello() -> impl Responder {
/// // this has path /api/hello
/// "Hello, world!"
/// }
/// }
/// # fn main() {}
/// ```
#[proc_macro_attribute]
pub fn scope(args: TokenStream, input: TokenStream) -> TokenStream {
scope::with_scope(args, input)
}
/// Marks async main function as the Actix Web system entry-point. /// Marks async main function as the Actix Web system entry-point.
/// ///
/// Note that Actix Web also works under `#[tokio::main]` since version 4.0. However, this macro is /// Note that Actix Web also works under `#[tokio::main]` since version 4.0. However, this macro is
@ -240,3 +278,15 @@ pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
output.extend(item); output.extend(item);
output output
} }
/// Converts the error to a token stream and appends it to the original input.
///
/// Returning the original input in addition to the error is good for IDEs which can gracefully
/// recover and show more precise errors within the macro body.
///
/// See <https://github.com/rust-analyzer/rust-analyzer/issues/10468> for more info.
fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream {
let compile_err = TokenStream::from(err.to_compile_error());
item.extend(compile_err);
item
}

View File

@ -6,10 +6,12 @@ use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt}; use quote::{quote, ToTokens, TokenStreamExt};
use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token}; use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token};
use crate::input_and_compile_error;
#[derive(Debug)] #[derive(Debug)]
pub struct RouteArgs { pub struct RouteArgs {
path: syn::LitStr, pub(crate) path: syn::LitStr,
options: Punctuated<syn::MetaNameValue, Token![,]>, pub(crate) options: Punctuated<syn::MetaNameValue, Token![,]>,
} }
impl syn::parse::Parse for RouteArgs { impl syn::parse::Parse for RouteArgs {
@ -78,7 +80,7 @@ macro_rules! standard_method_type {
} }
} }
fn from_path(method: &Path) -> Result<Self, ()> { pub(crate) fn from_path(method: &Path) -> Result<Self, ()> {
match () { match () {
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+ $(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
_ => Err(()), _ => Err(()),
@ -411,6 +413,13 @@ impl ToTokens for Route {
doc_attributes, doc_attributes,
} = self; } = self;
#[allow(unused_variables)] // used when force-pub feature is disabled
let vis = &ast.vis;
// TODO(breaking): remove this force-pub forwards-compatibility feature
#[cfg(feature = "compat-routing-macros-force-pub")]
let vis = syn::Visibility::Public(<Token![pub]>::default());
let registrations: TokenStream2 = args let registrations: TokenStream2 = args
.iter() .iter()
.map(|args| { .map(|args| {
@ -458,7 +467,7 @@ impl ToTokens for Route {
let stream = quote! { let stream = quote! {
#(#doc_attributes)* #(#doc_attributes)*
#[allow(non_camel_case_types, missing_docs)] #[allow(non_camel_case_types, missing_docs)]
pub struct #name; #vis struct #name;
impl ::actix_web::dev::HttpServiceFactory for #name { impl ::actix_web::dev::HttpServiceFactory for #name {
fn register(self, __config: &mut actix_web::dev::AppService) { fn register(self, __config: &mut actix_web::dev::AppService) {
@ -542,15 +551,3 @@ pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
Err(err) => input_and_compile_error(input, err), Err(err) => input_and_compile_error(input, err),
} }
} }
/// Converts the error to a token stream and appends it to the original input.
///
/// Returning the original input in addition to the error is good for IDEs which can gracefully
/// recover and show more precise errors within the macro body.
///
/// See <https://github.com/rust-analyzer/rust-analyzer/issues/10468> for more info.
fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream {
let compile_err = TokenStream::from(err.to_compile_error());
item.extend(compile_err);
item
}

View File

@ -0,0 +1,103 @@
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens as _};
use crate::{
input_and_compile_error,
route::{MethodType, RouteArgs},
};
pub fn with_scope(args: TokenStream, input: TokenStream) -> TokenStream {
match with_scope_inner(args, input.clone()) {
Ok(stream) => stream,
Err(err) => input_and_compile_error(input, err),
}
}
fn with_scope_inner(args: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
"missing arguments for scope macro, expected: #[scope(\"/prefix\")]",
));
}
let scope_prefix = syn::parse::<syn::LitStr>(args.clone()).map_err(|err| {
syn::Error::new(
err.span(),
"argument to scope macro is not a string literal, expected: #[scope(\"/prefix\")]",
)
})?;
let scope_prefix_value = scope_prefix.value();
if scope_prefix_value.ends_with('/') {
// trailing slashes cause non-obvious problems
// it's better to point them out to developers rather than
return Err(syn::Error::new(
scope_prefix.span(),
"scopes should not have trailing slashes; see https://docs.rs/actix-web/4/actix_web/struct.Scope.html#avoid-trailing-slashes",
));
}
let mut module = syn::parse::<syn::ItemMod>(input).map_err(|err| {
syn::Error::new(err.span(), "#[scope] macro must be attached to a module")
})?;
// modify any routing macros (method or route[s]) attached to
// functions by prefixing them with this scope macro's argument
if let Some((_, items)) = &mut module.content {
for item in items {
if let syn::Item::Fn(fun) = item {
fun.attrs = fun
.attrs
.iter()
.map(|attr| modify_attribute_with_scope(attr, &scope_prefix_value))
.collect();
}
}
}
Ok(module.to_token_stream().into())
}
/// Checks if the attribute is a method type and has a route path, then modifies it.
fn modify_attribute_with_scope(attr: &syn::Attribute, scope_path: &str) -> syn::Attribute {
match (attr.parse_args::<RouteArgs>(), attr.clone().meta) {
(Ok(route_args), syn::Meta::List(meta_list)) if has_allowed_methods_in_scope(attr) => {
let modified_path = format!("{}{}", scope_path, route_args.path.value());
let options_tokens: Vec<TokenStream2> = route_args
.options
.iter()
.map(|option| {
quote! { ,#option }
})
.collect();
let combined_options_tokens: TokenStream2 =
options_tokens
.into_iter()
.fold(TokenStream2::new(), |mut acc, ts| {
acc.extend(std::iter::once(ts));
acc
});
syn::Attribute {
meta: syn::Meta::List(syn::MetaList {
tokens: quote! { #modified_path #combined_options_tokens },
..meta_list.clone()
}),
..attr.clone()
}
}
_ => attr.clone(),
}
}
fn has_allowed_methods_in_scope(attr: &syn::Attribute) -> bool {
MethodType::from_path(attr.path()).is_ok()
|| attr.path().is_ident("route")
|| attr.path().is_ident("ROUTE")
}

View File

@ -0,0 +1,200 @@
use actix_web::{guard::GuardContext, http, http::header, web, App, HttpResponse, Responder};
use actix_web_codegen::{delete, get, post, route, routes, scope};
pub fn image_guard(ctx: &GuardContext) -> bool {
ctx.header::<header::Accept>()
.map(|h| h.preference() == "image/*")
.unwrap_or(false)
}
#[scope("/test")]
mod scope_module {
// ensure that imports can be brought into the scope
use super::*;
#[get("/test/guard", guard = "image_guard")]
pub async fn guard() -> impl Responder {
HttpResponse::Ok()
}
#[get("/test")]
pub async fn test() -> impl Responder {
HttpResponse::Ok().finish()
}
#[get("/twice-test/{value}")]
pub async fn twice(value: web::Path<String>) -> impl actix_web::Responder {
let int_value: i32 = value.parse().unwrap_or(0);
let doubled = int_value * 2;
HttpResponse::Ok().body(format!("Twice value: {}", doubled))
}
#[post("/test")]
pub async fn post() -> impl Responder {
HttpResponse::Ok().body("post works")
}
#[delete("/test")]
pub async fn delete() -> impl Responder {
"delete works"
}
#[route("/test", method = "PUT", method = "PATCH", method = "CUSTOM")]
pub async fn multiple_shared_path() -> impl Responder {
HttpResponse::Ok().finish()
}
#[routes]
#[head("/test1")]
#[connect("/test2")]
#[options("/test3")]
#[trace("/test4")]
pub async fn multiple_separate_paths() -> impl Responder {
HttpResponse::Ok().finish()
}
// test calling this from other mod scope with scope attribute...
pub fn mod_common(message: String) -> impl actix_web::Responder {
HttpResponse::Ok().body(message)
}
}
/// Scope doc string to check in cargo expand.
#[scope("/v1")]
mod mod_scope_v1 {
use super::*;
/// Route doc string to check in cargo expand.
#[get("/test")]
pub async fn test() -> impl Responder {
scope_module::mod_common("version1 works".to_string())
}
}
#[scope("/v2")]
mod mod_scope_v2 {
use super::*;
// check to make sure non-function tokens in the scope block are preserved...
enum TestEnum {
Works,
}
#[get("/test")]
pub async fn test() -> impl Responder {
// make sure this type still exists...
let test_enum = TestEnum::Works;
match test_enum {
TestEnum::Works => scope_module::mod_common("version2 works".to_string()),
}
}
}
#[actix_rt::test]
async fn scope_get_async() {
let srv = actix_test::start(|| App::new().service(scope_module::test));
let request = srv.request(http::Method::GET, srv.url("/test/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}
#[actix_rt::test]
async fn scope_get_param_async() {
let srv = actix_test::start(|| App::new().service(scope_module::twice));
let request = srv.request(http::Method::GET, srv.url("/test/twice-test/4"));
let mut response = request.send().await.unwrap();
let body = response.body().await.unwrap();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(body_str, "Twice value: 8");
}
#[actix_rt::test]
async fn scope_post_async() {
let srv = actix_test::start(|| App::new().service(scope_module::post));
let request = srv.request(http::Method::POST, srv.url("/test/test"));
let mut response = request.send().await.unwrap();
let body = response.body().await.unwrap();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(body_str, "post works");
}
#[actix_rt::test]
async fn multiple_shared_path_async() {
let srv = actix_test::start(|| App::new().service(scope_module::multiple_shared_path));
let request = srv.request(http::Method::PUT, srv.url("/test/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::PATCH, srv.url("/test/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}
#[actix_rt::test]
async fn multiple_multi_path_async() {
let srv = actix_test::start(|| App::new().service(scope_module::multiple_separate_paths));
let request = srv.request(http::Method::HEAD, srv.url("/test/test1"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::CONNECT, srv.url("/test/test2"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::OPTIONS, srv.url("/test/test3"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::TRACE, srv.url("/test/test4"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}
#[actix_rt::test]
async fn scope_delete_async() {
let srv = actix_test::start(|| App::new().service(scope_module::delete));
let request = srv.request(http::Method::DELETE, srv.url("/test/test"));
let mut response = request.send().await.unwrap();
let body = response.body().await.unwrap();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(body_str, "delete works");
}
#[actix_rt::test]
async fn scope_get_with_guard_async() {
let srv = actix_test::start(|| App::new().service(scope_module::guard));
let request = srv
.request(http::Method::GET, srv.url("/test/test/guard"))
.insert_header(("Accept", "image/*"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}
#[actix_rt::test]
async fn scope_v1_v2_async() {
let srv = actix_test::start(|| {
App::new()
.service(mod_scope_v1::test)
.service(mod_scope_v2::test)
});
let request = srv.request(http::Method::GET, srv.url("/v1/test"));
let mut response = request.send().await.unwrap();
let body = response.body().await.unwrap();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(body_str, "version1 works");
let request = srv.request(http::Method::GET, srv.url("/v2/test"));
let mut response = request.send().await.unwrap();
let body = response.body().await.unwrap();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(body_str, "version2 works");
}

View File

@ -18,6 +18,11 @@ fn compile_macros() {
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs"); t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs"); t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");
t.compile_fail("tests/trybuild/scope-on-handler.rs");
t.compile_fail("tests/trybuild/scope-missing-args.rs");
t.compile_fail("tests/trybuild/scope-invalid-args.rs");
t.compile_fail("tests/trybuild/scope-trailing-slash.rs");
t.pass("tests/trybuild/docstring-ok.rs"); t.pass("tests/trybuild/docstring-ok.rs");
t.pass("tests/trybuild/test-runtime.rs"); t.pass("tests/trybuild/test-runtime.rs");

View File

@ -20,10 +20,7 @@ error: custom attribute panicked
13 | #[get("/{}")] 13 | #[get("/{}")]
| ^^^^^^^^^^^^^ | ^^^^^^^^^^^^^
| |
= help: message: Wrong path pattern: "/{}" regex parse error: = help: message: Wrong path pattern: "/{}" empty capture group names are not allowed
((?s-m)^/(?P<>[^/]+))$
^
error: empty capture group name
error: custom attribute panicked error: custom attribute panicked
--> $DIR/route-malformed-path-fail.rs:23:1 --> $DIR/route-malformed-path-fail.rs:23:1

View File

@ -0,0 +1,14 @@
use actix_web_codegen::scope;
const PATH: &str = "/api";
#[scope(PATH)]
mod api_const {}
#[scope(true)]
mod api_bool {}
#[scope(123)]
mod api_num {}
fn main() {}

View File

@ -0,0 +1,17 @@
error: argument to scope macro is not a string literal, expected: #[scope("/prefix")]
--> tests/trybuild/scope-invalid-args.rs:5:9
|
5 | #[scope(PATH)]
| ^^^^
error: argument to scope macro is not a string literal, expected: #[scope("/prefix")]
--> tests/trybuild/scope-invalid-args.rs:8:9
|
8 | #[scope(true)]
| ^^^^
error: argument to scope macro is not a string literal, expected: #[scope("/prefix")]
--> tests/trybuild/scope-invalid-args.rs:11:9
|
11 | #[scope(123)]
| ^^^

View File

@ -0,0 +1,6 @@
use actix_web_codegen::scope;
#[scope]
mod api {}
fn main() {}

View File

@ -0,0 +1,7 @@
error: missing arguments for scope macro, expected: #[scope("/prefix")]
--> tests/trybuild/scope-missing-args.rs:3:1
|
3 | #[scope]
| ^^^^^^^^
|
= note: this error originates in the attribute macro `scope` (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@ -0,0 +1,8 @@
use actix_web_codegen::scope;
#[scope("/api")]
async fn index() -> &'static str {
"Hello World!"
}
fn main() {}

View File

@ -0,0 +1,5 @@
error: #[scope] macro must be attached to a module
--> tests/trybuild/scope-on-handler.rs:4:1
|
4 | async fn index() -> &'static str {
| ^^^^^

View File

@ -0,0 +1,6 @@
use actix_web_codegen::scope;
#[scope("/api/")]
mod api {}
fn main() {}

View File

@ -0,0 +1,5 @@
error: scopes should not have trailing slashes; see https://docs.rs/actix-web/4/actix_web/struct.Scope.html#avoid-trailing-slashes
--> tests/trybuild/scope-trailing-slash.rs:3:9
|
3 | #[scope("/api/")]
| ^^^^^^^

View File

@ -2,6 +2,22 @@
## Unreleased ## Unreleased
### Fixed
- `ConnectionInfo::realip_remote_addr()` now handles IPv6 addresses from `Forwarded` header correctly. Previously, it sometimes returned the forwarded port as well.
## 4.7.0
### Added
- Add `#[scope]` macro.
- Add `middleware::Identity` type.
- Add `CustomizeResponder::add_cookie()` method.
- Add `guard::GuardContext::app_data()` method.
- Add `compat-routing-macros-force-pub` crate feature which (on-by-default) which, when disabled, causes handlers to inherit their attached function's visibility.
- Add `compat` crate feature group (on-by-default) which, when disabled, helps with transitioning to some planned v5.0 breaking changes, starting only with `compat-routing-macros-force-pub`.
- Implement `From<Box<dyn ResponseError>>` for `Error`.
## 4.6.0 ## 4.6.0
### Added ### Added

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.6.0" version = "4.7.0"
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
@ -35,13 +35,21 @@ features = [
"secure-cookies", "secure-cookies",
] ]
[lib] [lib]
name = "actix_web" name = "actix_web"
path = "src/lib.rs" path = "src/lib.rs"
[features] [features]
default = ["macros", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "http2", "unicode"] default = [
"macros",
"compress-brotli",
"compress-gzip",
"compress-zstd",
"cookies",
"http2",
"unicode",
"compat",
]
# Brotli algorithm content-encoding support # Brotli algorithm content-encoding support
compress-brotli = ["actix-http/compress-brotli", "__compress"] compress-brotli = ["actix-http/compress-brotli", "__compress"]
@ -51,14 +59,15 @@ compress-gzip = ["actix-http/compress-gzip", "__compress"]
compress-zstd = ["actix-http/compress-zstd", "__compress"] compress-zstd = ["actix-http/compress-zstd", "__compress"]
# Routing and runtime proc macros # Routing and runtime proc macros
macros = ["actix-macros", "actix-web-codegen"] macros = ["dep:actix-macros", "dep:actix-web-codegen"]
# Cookies support # Cookies support
cookies = ["cookie"] cookies = ["dep:cookie"]
# Secure & signed cookies # Secure & signed cookies
secure-cookies = ["cookies", "cookie/secure"] secure-cookies = ["cookies", "cookie/secure"]
# HTTP/2 support (including h2c).
http2 = ["actix-http/http2"] http2 = ["actix-http/http2"]
# TLS via OpenSSL # TLS via OpenSSL
@ -85,6 +94,14 @@ __compress = []
# io-uring feature only available for Linux OSes. # io-uring feature only available for Linux OSes.
experimental-io-uring = ["actix-server/io-uring"] experimental-io-uring = ["actix-server/io-uring"]
# Feature group which, when disabled, helps migrate code to v5.0.
compat = [
"compat-routing-macros-force-pub",
]
# Opt-out forwards-compatibility for handler visibility inheritance fix.
compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"]
[dependencies] [dependencies]
actix-codec = "0.5" actix-codec = "0.5"
actix-macros = { version = "0.2.3", optional = true } actix-macros = { version = "0.2.3", optional = true }
@ -95,8 +112,8 @@ actix-utils = "3"
actix-tls = { version = "3.4", default-features = false, optional = true } actix-tls = { version = "3.4", default-features = false, optional = true }
actix-http = { version = "3.7", features = ["ws"] } actix-http = { version = "3.7", features = ["ws"] }
actix-router = { version = "0.5", default-features = false, features = ["http"] } actix-router = { version = "0.5.3", default-features = false, features = ["http"] }
actix-web-codegen = { version = "4.2", optional = true } actix-web-codegen = { version = "4.3", optional = true, default-features = false }
ahash = "0.8" ahash = "0.8"
bytes = "1" bytes = "1"
@ -130,6 +147,7 @@ awc = { version = "3", features = ["openssl"] }
brotli = "6" brotli = "6"
const-str = "0.5" const-str = "0.5"
core_affinity = "0.8"
criterion = { version = "0.5", features = ["html_reports"] } criterion = { version = "0.5", features = ["html_reports"] }
env_logger = "0.11" env_logger = "0.11"
flate2 = "1.0.13" flate2 = "1.0.13"

View File

@ -8,10 +8,10 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.6.0)](https://docs.rs/actix-web/4.6.0) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.7.0)](https://docs.rs/actix-web/4.7.0)
![MSRV](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![MSRV](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.6.0/status.svg)](https://deps.rs/crate/actix-web/4.6.0) [![Dependency Status](https://deps.rs/crate/actix-web/4.7.0/status.svg)](https://deps.rs/crate/actix-web/4.7.0)
<br /> <br />
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) [![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)

View File

@ -0,0 +1,41 @@
use std::{
io,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
thread,
};
use actix_web::{middleware, web, App, HttpServer};
async fn hello() -> &'static str {
"Hello world!"
}
#[actix_web::main]
async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let core_ids = core_affinity::get_core_ids().unwrap();
let n_core_ids = core_ids.len();
let next_core_id = Arc::new(AtomicUsize::new(0));
HttpServer::new(move || {
let pin = Arc::clone(&next_core_id).fetch_add(1, Ordering::AcqRel);
log::info!(
"setting CPU affinity for worker {}: pinning to core {}",
thread::current().name().unwrap(),
pin,
);
core_affinity::set_for_current(core_ids[pin]);
App::new()
.wrap(middleware::Logger::default())
.service(web::resource("/").get(hello))
})
.bind(("127.0.0.1", 8080))?
.workers(n_core_ids)
.run()
.await
}

View File

@ -112,8 +112,8 @@ where
/// }) /// })
/// ``` /// ```
#[doc(alias = "manage")] #[doc(alias = "manage")]
pub fn app_data<U: 'static>(mut self, ext: U) -> Self { pub fn app_data<U: 'static>(mut self, data: U) -> Self {
self.extensions.insert(ext); self.extensions.insert(data);
self self
} }

View File

@ -6,7 +6,7 @@ use crate::{HttpResponse, ResponseError};
/// General purpose Actix Web error. /// General purpose Actix Web error.
/// ///
/// An Actix Web error is used to carry errors from `std::error` through actix in a convenient way. /// An Actix Web error is used to carry errors from `std::error` through Actix in a convenient way.
/// It can be created through converting errors with `into()`. /// It can be created through converting errors with `into()`.
/// ///
/// Whenever it is created from an external object a response error is created for it that can be /// Whenever it is created from an external object a response error is created for it that can be
@ -14,6 +14,7 @@ use crate::{HttpResponse, ResponseError};
/// you can always get a `ResponseError` reference from it. /// you can always get a `ResponseError` reference from it.
pub struct Error { pub struct Error {
cause: Box<dyn ResponseError>, cause: Box<dyn ResponseError>,
response_mappers: Vec<Box<dyn Fn(HttpResponse) -> HttpResponse>>,
} }
impl Error { impl Error {
@ -29,7 +30,20 @@ impl Error {
/// Shortcut for creating an `HttpResponse`. /// Shortcut for creating an `HttpResponse`.
pub fn error_response(&self) -> HttpResponse { pub fn error_response(&self) -> HttpResponse {
self.cause.error_response() let mut res = self.cause.error_response();
for mapper in &self.response_mappers {
res = (mapper)(res);
}
res
}
pub fn add_mapper<F, B>(&mut self, mapper: F)
where
F: Fn(HttpResponse) -> HttpResponse + 'static,
{
self.response_mappers.push(Box::new(mapper))
} }
} }
@ -56,10 +70,17 @@ impl<T: ResponseError + 'static> From<T> for Error {
fn from(err: T) -> Error { fn from(err: T) -> Error {
Error { Error {
cause: Box::new(err), cause: Box::new(err),
response_mappers: Vec::new(),
} }
} }
} }
impl From<Box<dyn ResponseError>> for Error {
fn from(value: Box<dyn ResponseError>) -> Self {
Error { cause: value }
}
}
impl From<Error> for Response<BoxBody> { impl From<Error> for Response<BoxBody> {
fn from(err: Error) -> Response<BoxBody> { fn from(err: Error) -> Response<BoxBody> {
err.error_response().into() err.error_response().into()

View File

@ -110,6 +110,12 @@ impl<'a> GuardContext<'a> {
pub fn header<H: Header>(&self) -> Option<H> { pub fn header<H: Header>(&self) -> Option<H> {
H::parse(self.req).ok() H::parse(self.req).ok()
} }
/// Counterpart to [HttpRequest::app_data](crate::HttpRequest::app_data).
#[inline]
pub fn app_data<T: 'static>(&self) -> Option<&T> {
self.req.app_data()
}
} }
/// Interface for routing guards. /// Interface for routing guards.
@ -512,4 +518,18 @@ mod tests {
.to_srv_request(); .to_srv_request();
assert!(guard.check(&req.guard_ctx())); assert!(guard.check(&req.guard_ctx()));
} }
#[test]
fn app_data() {
const TEST_VALUE: u32 = 42;
let guard = fn_guard(|ctx| dbg!(ctx.app_data::<u32>()) == Some(&TEST_VALUE));
let req = TestRequest::default().app_data(TEST_VALUE).to_srv_request();
assert!(guard.check(&req.guard_ctx()));
let req = TestRequest::default()
.app_data(TEST_VALUE * 2)
.to_srv_request();
assert!(!guard.check(&req.guard_ctx()));
}
} }

View File

@ -21,6 +21,19 @@ fn unquote(val: &str) -> &str {
val.trim().trim_start_matches('"').trim_end_matches('"') val.trim().trim_start_matches('"').trim_end_matches('"')
} }
/// Remove port and IPv6 square brackets from a peer specification.
fn bare_address(val: &str) -> &str {
if val.starts_with('[') {
val.split("]:")
.next()
.map(|s| s.trim_start_matches('[').trim_end_matches(']'))
// This shouldn't *actually* ever happen
.unwrap_or(val)
} else {
val.split(':').next().unwrap_or(val)
}
}
/// Extracts and trims first value for given header name. /// Extracts and trims first value for given header name.
fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<&'a str> { fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<&'a str> {
let hdr = req.headers.get(name)?.to_str().ok()?; let hdr = req.headers.get(name)?.to_str().ok()?;
@ -100,7 +113,7 @@ impl ConnectionInfo {
// --- https://datatracker.ietf.org/doc/html/rfc7239#section-5.2 // --- https://datatracker.ietf.org/doc/html/rfc7239#section-5.2
match name.trim().to_lowercase().as_str() { match name.trim().to_lowercase().as_str() {
"for" => realip_remote_addr.get_or_insert_with(|| unquote(val)), "for" => realip_remote_addr.get_or_insert_with(|| bare_address(unquote(val))),
"proto" => scheme.get_or_insert_with(|| unquote(val)), "proto" => scheme.get_or_insert_with(|| unquote(val)),
"host" => host.get_or_insert_with(|| unquote(val)), "host" => host.get_or_insert_with(|| unquote(val)),
"by" => { "by" => {
@ -368,16 +381,25 @@ mod tests {
.insert_header((header::FORWARDED, r#"for="192.0.2.60:8080""#)) .insert_header((header::FORWARDED, r#"for="192.0.2.60:8080""#))
.to_http_request(); .to_http_request();
let info = req.connection_info(); let info = req.connection_info();
assert_eq!(info.realip_remote_addr(), Some("192.0.2.60:8080")); assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
} }
#[test] #[test]
fn forwarded_for_ipv6() { fn forwarded_for_ipv6() {
let req = TestRequest::default()
.insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]""#))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.realip_remote_addr(), Some("2001:db8:cafe::17"));
}
#[test]
fn forwarded_for_ipv6_with_port() {
let req = TestRequest::default() let req = TestRequest::default()
.insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]:4711""#)) .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]:4711""#))
.to_http_request(); .to_http_request();
let info = req.connection_info(); let info = req.connection_info();
assert_eq!(info.realip_remote_addr(), Some("[2001:db8:cafe::17]:4711")); assert_eq!(info.realip_remote_addr(), Some("2001:db8:cafe::17"));
} }
#[test] #[test]

View File

@ -145,5 +145,6 @@ codegen_reexport!(delete);
codegen_reexport!(trace); codegen_reexport!(trace);
codegen_reexport!(connect); codegen_reexport!(connect);
codegen_reexport!(options); codegen_reexport!(options);
codegen_reexport!(scope);
pub(crate) type BoxError = Box<dyn std::error::Error>; pub(crate) type BoxError = Box<dyn std::error::Error>;

View File

@ -38,15 +38,6 @@ pub struct Compat<T> {
transform: T, transform: T,
} }
#[cfg(test)]
impl Compat<super::Noop> {
pub(crate) fn noop() -> Self {
Self {
transform: super::Noop,
}
}
}
impl<T> Compat<T> { impl<T> Compat<T> {
/// Wrap a middleware to give it broader compatibility. /// Wrap a middleware to give it broader compatibility.
pub fn new(middleware: T) -> Self { pub fn new(middleware: T) -> Self {
@ -152,7 +143,7 @@ mod tests {
use crate::{ use crate::{
dev::ServiceRequest, dev::ServiceRequest,
http::StatusCode, http::StatusCode,
middleware::{self, Condition, Logger}, middleware::{self, Condition, Identity, Logger},
test::{self, call_service, init_service, TestRequest}, test::{self, call_service, init_service, TestRequest},
web, App, HttpResponse, web, App, HttpResponse,
}; };
@ -225,7 +216,7 @@ mod tests {
async fn compat_noop_is_noop() { async fn compat_noop_is_noop() {
let srv = test::ok_service(); let srv = test::ok_service();
let mw = Compat::noop() let mw = Compat::new(Identity)
.new_transform(srv.into_service()) .new_transform(srv.into_service())
.await .await
.unwrap(); .unwrap();

View File

@ -141,7 +141,7 @@ mod tests {
header::{HeaderValue, CONTENT_TYPE}, header::{HeaderValue, CONTENT_TYPE},
StatusCode, StatusCode,
}, },
middleware::{self, ErrorHandlerResponse, ErrorHandlers}, middleware::{self, ErrorHandlerResponse, ErrorHandlers, Identity},
test::{self, TestRequest}, test::{self, TestRequest},
web::Bytes, web::Bytes,
HttpResponse, HttpResponse,
@ -158,7 +158,7 @@ mod tests {
#[test] #[test]
fn compat_with_builtin_middleware() { fn compat_with_builtin_middleware() {
let _ = Condition::new(true, middleware::Compat::noop()); let _ = Condition::new(true, middleware::Compat::new(Identity));
let _ = Condition::new(true, middleware::Logger::default()); let _ = Condition::new(true, middleware::Logger::default());
let _ = Condition::new(true, middleware::Compress::default()); let _ = Condition::new(true, middleware::Compress::default());
let _ = Condition::new(true, middleware::NormalizePath::trim()); let _ = Condition::new(true, middleware::NormalizePath::trim());

View File

@ -2,35 +2,39 @@
use actix_utils::future::{ready, Ready}; use actix_utils::future::{ready, Ready};
use crate::dev::{Service, Transform}; use crate::dev::{forward_ready, Service, Transform};
/// A no-op middleware that passes through request and response untouched. /// A no-op middleware that passes through request and response untouched.
pub(crate) struct Noop; #[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct Identity;
impl<S: Service<Req>, Req> Transform<S, Req> for Noop { impl<S: Service<Req>, Req> Transform<S, Req> for Identity {
type Response = S::Response; type Response = S::Response;
type Error = S::Error; type Error = S::Error;
type Transform = NoopService<S>; type Transform = IdentityMiddleware<S>;
type InitError = (); type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>; type Future = Ready<Result<Self::Transform, Self::InitError>>;
#[inline]
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(NoopService { service })) ready(Ok(IdentityMiddleware { service }))
} }
} }
#[doc(hidden)] #[doc(hidden)]
pub(crate) struct NoopService<S> { pub struct IdentityMiddleware<S> {
service: S, service: S,
} }
impl<S: Service<Req>, Req> Service<Req> for NoopService<S> { impl<S: Service<Req>, Req> Service<Req> for IdentityMiddleware<S> {
type Response = S::Response; type Response = S::Response;
type Error = S::Error; type Error = S::Error;
type Future = S::Future; type Future = S::Future;
crate::dev::forward_ready!(service); forward_ready!(service);
#[inline]
fn call(&self, req: Req) -> Self::Future { fn call(&self, req: Req) -> Self::Future {
self.service.call(req) self.service.call(req)
} }

View File

@ -218,31 +218,27 @@
//! [lab_from_fn]: https://docs.rs/actix-web-lab/latest/actix_web_lab/middleware/fn.from_fn.html //! [lab_from_fn]: https://docs.rs/actix-web-lab/latest/actix_web_lab/middleware/fn.from_fn.html
mod compat; mod compat;
#[cfg(feature = "__compress")]
mod compress;
mod condition; mod condition;
mod default_headers; mod default_headers;
mod err_handlers; mod err_handlers;
mod identity;
mod logger; mod logger;
#[cfg(test)]
mod noop;
mod normalize; mod normalize;
#[cfg(test)] #[cfg(feature = "__compress")]
pub(crate) use self::noop::Noop; pub use self::compress::Compress;
pub use self::{ pub use self::{
compat::Compat, compat::Compat,
condition::Condition, condition::Condition,
default_headers::DefaultHeaders, default_headers::DefaultHeaders,
err_handlers::{ErrorHandlerResponse, ErrorHandlers}, err_handlers::{ErrorHandlerResponse, ErrorHandlers},
identity::Identity,
logger::Logger, logger::Logger,
normalize::{NormalizePath, TrailingSlash}, normalize::{NormalizePath, TrailingSlash},
}; };
#[cfg(feature = "__compress")]
mod compress;
#[cfg(feature = "__compress")]
pub use self::compress::Compress;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -7,7 +7,7 @@ use actix_http::{
use crate::{HttpRequest, HttpResponse, Responder}; use crate::{HttpRequest, HttpResponse, Responder};
/// Allows overriding status code and headers for a [`Responder`]. /// Allows overriding status code and headers (including cookies) for a [`Responder`].
/// ///
/// Created by calling the [`customize`](Responder::customize) method on a [`Responder`] type. /// Created by calling the [`customize`](Responder::customize) method on a [`Responder`] type.
pub struct CustomizeResponder<R> { pub struct CustomizeResponder<R> {
@ -137,6 +137,29 @@ impl<R: Responder> CustomizeResponder<R> {
Some(&mut self.inner) Some(&mut self.inner)
} }
} }
/// Appends a `cookie` to the final response.
///
/// # Errors
///
/// Final response will be an error if `cookie` cannot be converted into a valid header value.
#[cfg(feature = "cookies")]
pub fn add_cookie(mut self, cookie: &crate::cookie::Cookie<'_>) -> Self {
use actix_http::header::{TryIntoHeaderValue as _, SET_COOKIE};
if let Some(inner) = self.inner() {
match cookie.to_string().try_into_value() {
Ok(val) => {
inner.append_headers.append(SET_COOKIE, val);
}
Err(err) => {
self.error = Some(err.into());
}
}
}
self
}
} }
impl<T> Responder for CustomizeResponder<T> impl<T> Responder for CustomizeResponder<T>
@ -175,6 +198,7 @@ mod tests {
use super::*; use super::*;
use crate::{ use crate::{
cookie::Cookie,
http::header::{HeaderValue, CONTENT_TYPE}, http::header::{HeaderValue, CONTENT_TYPE},
test::TestRequest, test::TestRequest,
}; };
@ -209,6 +233,22 @@ mod tests {
to_bytes(res.into_body()).await.unwrap(), to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"), Bytes::from_static(b"test"),
); );
let res = "test"
.to_string()
.customize()
.add_cookie(&Cookie::new("name", "value"))
.respond_to(&req);
assert!(res.status().is_success());
assert_eq!(
res.cookies().collect::<Vec<Cookie<'_>>>(),
vec![Cookie::new("name", "value")],
);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
} }
#[actix_rt::test] #[actix_rt::test]

View File

@ -64,7 +64,7 @@ compress-gzip = ["actix-http/compress-gzip", "__compress"]
compress-zstd = ["actix-http/compress-zstd", "__compress"] compress-zstd = ["actix-http/compress-zstd", "__compress"]
# Cookie parsing and cookie jar # Cookie parsing and cookie jar
cookies = ["cookie"] cookies = ["dep:cookie"]
# Use `trust-dns-resolver` crate as DNS resolver # Use `trust-dns-resolver` crate as DNS resolver
trust-dns = ["trust-dns-resolver"] trust-dns = ["trust-dns-resolver"]
@ -92,7 +92,7 @@ cfg-if = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] }
h2 = "0.3.24" h2 = "0.3.26"
http = "0.2.7" http = "0.2.7"
itoa = "1" itoa = "1"
log =" 0.4" log =" 0.4"

View File

@ -1080,7 +1080,7 @@ mod resolver {
// resolver struct is cached in thread local so new clients can reuse the existing instance // resolver struct is cached in thread local so new clients can reuse the existing instance
thread_local! { thread_local! {
static TRUST_DNS_RESOLVER: RefCell<Option<Resolver>> = RefCell::new(None); static TRUST_DNS_RESOLVER: RefCell<Option<Resolver>> = const { RefCell::new(None) };
} }
// get from thread local or construct a new trust-dns resolver. // get from thread local or construct a new trust-dns resolver.

View File

@ -4,7 +4,7 @@ _list:
# Format workspace. # Format workspace.
fmt: fmt:
cargo +nightly fmt cargo +nightly fmt
npx -y prettier --write $(fd --type=file --hidden --extension=md --extension=yml) fd --hidden --type=file --extension=md --extension=yml --exec-batch npx -y prettier --write
# Downgrade dev-dependencies necessary to run MSRV checks/tests. # Downgrade dev-dependencies necessary to run MSRV checks/tests.
[private] [private]
@ -32,6 +32,10 @@ all_crate_features := if os() == "linux" {
"--features='" + non_linux_all_features_list + "'" "--features='" + non_linux_all_features_list + "'"
} }
# Run Clippy over workspace.
clippy toolchain="":
cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }}
# Test workspace using MSRV. # Test workspace using MSRV.
test-msrv: downgrade-for-msrv (test msrv_rustup) test-msrv: downgrade-for-msrv (test msrv_rustup)