Repository: swaggest/rest
Pull request #55 (2022-01-26 21:33:35)

Deprecate resttest package, upgrade dependencies

github-actions[bot]: ### Lines Of Code

| Language | Files |    Lines    |    Code     | Comments  |   Blanks    | Complexity |      Bytes      |
|----------|-------|-------------|-------------|-----------|-------------|------------|-----------------|
| Go       | 95    | 6218 (-850) | 4518 (-612) | 414 (-58) | 1286 (-180) | 703 (-197) | 151.6K (-18.4K) |
<!-- Sticky Pull Request CommentLOC -->
github-actions[bot]: ### Unit Test Coverage
total:	(statements)	82.7%
<details><summary>Coverage diff with base branch</summary>

```diff
140,163d139
< github.com/swaggest/rest/resttest/client.go	SetBaseURL	66.7%
< github.com/swaggest/rest/resttest/client.go	Reset	100.0%
< github.com/swaggest/rest/resttest/client.go	WithMethod	100.0%
< github.com/swaggest/rest/resttest/client.go	WithPath	0.0%
< github.com/swaggest/rest/resttest/client.go	WithURI	100.0%
< github.com/swaggest/rest/resttest/client.go	WithBody	100.0%
< github.com/swaggest/rest/resttest/client.go	WithContentType	100.0%
< github.com/swaggest/rest/resttest/client.go	WithHeader	100.0%
< github.com/swaggest/rest/resttest/client.go	WithCookie	100.0%
< github.com/swaggest/rest/resttest/client.go	do	81.1%
< github.com/swaggest/rest/resttest/client.go	checkResponses	76.2%
< github.com/swaggest/rest/resttest/client.go	doOnce	95.7%
< github.com/swaggest/rest/resttest/client.go	ExpectResponseStatus	80.0%
< github.com/swaggest/rest/resttest/client.go	ExpectResponseHeader	40.0%
< github.com/swaggest/rest/resttest/client.go	CheckUnexpectedOtherResponses	66.7%
< github.com/swaggest/rest/resttest/client.go	ExpectNoOtherResponses	50.0%
< github.com/swaggest/rest/resttest/client.go	ExpectOtherResponsesStatus	57.1%
< github.com/swaggest/rest/resttest/client.go	ExpectOtherResponsesHeader	57.1%
< github.com/swaggest/rest/resttest/client.go	assertResponseCode	100.0%
< github.com/swaggest/rest/resttest/client.go	assertResponseHeader	71.4%
< github.com/swaggest/rest/resttest/client.go	ExpectResponseBody	80.0%
< github.com/swaggest/rest/resttest/client.go	ExpectOtherResponsesBody	50.0%
< github.com/swaggest/rest/resttest/client.go	checkBody	57.1%
< github.com/swaggest/rest/resttest/client.go	Concurrently	75.0%
165,176d140
< github.com/swaggest/rest/resttest/server.go	Expect	100.0%
< github.com/swaggest/rest/resttest/server.go	ExpectAsync	100.0%
< github.com/swaggest/rest/resttest/server.go	Close	100.0%
< github.com/swaggest/rest/resttest/server.go	prepareBody	80.0%
< github.com/swaggest/rest/resttest/server.go	writeResponse	93.3%
< github.com/swaggest/rest/resttest/server.go	checkAsync	68.8%
< github.com/swaggest/rest/resttest/server.go	ServeHTTP	80.8%
< github.com/swaggest/rest/resttest/server.go	checkBody	83.3%
< github.com/swaggest/rest/resttest/server.go	checkRequest	84.2%
< github.com/swaggest/rest/resttest/server.go	checkFail	66.7%
< github.com/swaggest/rest/resttest/server.go	ResetExpectations	100.0%
< github.com/swaggest/rest/resttest/server.go	ExpectationsWereMet	83.3%
185c149
< total:	(statements)	81.8%
---
> total:	(statements)	82.7%
```
</details>
<!-- Sticky Pull Request Commentunit-test -->
github-actions[bot]: ### Go API Changes

<pre>
# github.com/swaggest/rest/resttest
## incompatible changes
NewServerMock: changed from func() (*ServerMock, string) to func() (*github.com/bool64/httpmock.Server, string)
ServerMock: changed from ServerMock to github.com/bool64/httpmock.Server

# summary
Inferred base version: v0.2.18
Suggested version: v0.3.0
</pre>
<!-- Sticky Pull Request Commentgorelease -->
codecov[bot]: # [Codecov](https://codecov.io/gh/swaggest/rest/pull/55?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) Report
> Merging [#55](https://codecov.io/gh/swaggest/rest/pull/55?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (25d6df5) into [master](https://codecov.io/gh/swaggest/rest/commit/144157d4976a45b0ca59516162f40035668dfe1c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (144157d) will **increase** coverage by `2.12%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/swaggest/rest/pull/55/graphs/tree.svg?width=650&height=150&src=pr&token=OpLHxxPdmn&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest)](https://codecov.io/gh/swaggest/rest/pull/55?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest)

```diff
@@            Coverage Diff             @@
##           master      #55      +/-   ##
==========================================
+ Coverage   74.05%   76.17%   +2.12%     
==========================================
  Files          26       26              
  Lines        1611     1272     -339     
==========================================
- Hits         1193      969     -224     
+ Misses        261      188      -73     
+ Partials      157      115      -42     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `76.17% <100.00%> (+2.12%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/swaggest/rest/pull/55?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [resttest/client.go](https://codecov.io/gh/swaggest/rest/pull/55/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-cmVzdHRlc3QvY2xpZW50Lmdv) | `100.00% <100.00%> (+35.82%)` | :arrow_up: |
| [resttest/server.go](https://codecov.io/gh/swaggest/rest/pull/55/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-cmVzdHRlc3Qvc2VydmVyLmdv) | `100.00% <100.00%> (+30.71%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/swaggest/rest/pull/55?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/swaggest/rest/pull/55?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest). Last update [144157d...25d6df5](https://codecov.io/gh/swaggest/rest/pull/55?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest).

github-actions[bot]: ### Benchmark Result
<details><summary>Benchmark diff with base branch</summary>

```
name                                    old time/op    new time/op    delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2    1.57µs ± 5%    1.56µs ± 2%   ~     (p=0.952 n=5+5)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                           827ns ± 3%     829ns ± 4%   ~     (p=1.000 n=5+5)
DecoderFunc_Decode-2                      2.42µs ± 3%    2.39µs ± 1%   ~     (p=0.151 n=5+5)
Decoder_Decode_json-2                     25.9µs ± 3%    25.7µs ± 3%   ~     (p=0.548 n=5+5)
Decoder_Decode_queryObject-2              5.62µs ± 2%    5.65µs ± 2%   ~     (p=0.690 n=5+5)
Decoder_Decode_jsonParam-2                2.07µs ± 2%    2.11µs ± 2%   ~     (p=0.056 n=5+5)
DecoderFactory_SetDecoderFunc-2           1.96µs ± 4%    2.01µs ± 3%   ~     (p=0.095 n=5+5)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              13.7µs ± 3%    13.7µs ± 1%   ~     (p=1.000 n=5+5)
Middleware_control-2                      3.77µs ± 6%    3.87µs ± 2%   ~     (p=0.151 n=5+5)

name                                    old alloc/op   new alloc/op   delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2    2.46kB ± 0%    2.46kB ± 0%   ~     (all equal)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                            440B ± 0%      440B ± 0%   ~     (all equal)
DecoderFunc_Decode-2                      1.51kB ± 0%    1.51kB ± 0%   ~     (all equal)
Decoder_Decode_json-2                     12.2kB ± 0%    12.2kB ± 0%   ~     (all equal)
Decoder_Decode_queryObject-2              2.00kB ± 0%    2.00kB ± 0%   ~     (all equal)
Decoder_Decode_jsonParam-2                  736B ± 0%      736B ± 0%   ~     (all equal)
DecoderFactory_SetDecoderFunc-2           1.02kB ± 0%    1.02kB ± 0%   ~     (all equal)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              1.15kB ± 2%    1.16kB ± 3%   ~     (p=0.952 n=5+5)
Middleware_control-2                      11.2kB ± 0%    11.2kB ± 0%   ~     (all equal)

name                                    old allocs/op  new allocs/op  delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2      8.00 ± 0%      8.00 ± 0%   ~     (all equal)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                            4.00 ± 0%      4.00 ± 0%   ~     (all equal)
DecoderFunc_Decode-2                        12.0 ± 0%      12.0 ± 0%   ~     (all equal)
Decoder_Decode_json-2                        177 ± 0%       177 ± 0%   ~     (all equal)
Decoder_Decode_queryObject-2                36.0 ± 0%      36.0 ± 0%   ~     (all equal)
Decoder_Decode_jsonParam-2                  13.0 ± 0%      13.0 ± 0%   ~     (all equal)
DecoderFactory_SetDecoderFunc-2             16.0 ± 0%      16.0 ± 0%   ~     (all equal)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                                11.0 ± 0%      11.0 ± 0%   ~     (all equal)
Middleware_control-2                        9.00 ± 0%      9.00 ± 0%   ~     (all equal)
```
</details>

<details><summary>Benchmark result</summary>

```
name                                    time/op
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2  1.56µs ± 2%
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                         829ns ± 4%
DecoderFunc_Decode-2                    2.39µs ± 1%
Decoder_Decode_json-2                   25.7µs ± 3%
Decoder_Decode_queryObject-2            5.65µs ± 2%
Decoder_Decode_jsonParam-2              2.11µs ± 2%
DecoderFactory_SetDecoderFunc-2         2.01µs ± 3%
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                            13.7µs ± 1%
Middleware_control-2                    3.87µs ± 2%

name                                    alloc/op
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2  2.46kB ± 0%
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                          440B ± 0%
DecoderFunc_Decode-2                    1.51kB ± 0%
Decoder_Decode_json-2                   12.2kB ± 0%
Decoder_Decode_queryObject-2            2.00kB ± 0%
Decoder_Decode_jsonParam-2                736B ± 0%
DecoderFactory_SetDecoderFunc-2         1.02kB ± 0%
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                            1.16kB ± 3%
Middleware_control-2                    11.2kB ± 0%

name                                    allocs/op
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2    8.00 ± 0%
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                          4.00 ± 0%
DecoderFunc_Decode-2                      12.0 ± 0%
Decoder_Decode_json-2                      177 ± 0%
Decoder_Decode_queryObject-2              36.0 ± 0%
Decoder_Decode_jsonParam-2                13.0 ± 0%
DecoderFactory_SetDecoderFunc-2           16.0 ± 0%
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              11.0 ± 0%
Middleware_control-2                      9.00 ± 0%
```
</details>
<!-- Sticky Pull Request Commentbench -->
github-actions[bot]: ### Examples Benchmark Result
<details><summary>Benchmark diff with base branch</summary>

```
name                       old time/op    new time/op    delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                38.0µs ± 6%    36.8µs ± 1%    ~     (p=0.222 n=5+5)
_directGzipHead-2            37.0µs ± 7%    38.1µs ±14%    ~     (p=0.548 n=5+5)
_noDirectGzip-2               174µs ± 1%     173µs ± 1%    ~     (p=0.690 n=5+5)
_directGzip_decode-2          545µs ± 2%     547µs ± 3%    ~     (p=0.548 n=5+5)
_noDirectGzip_decode-2        172µs ± 0%     174µs ± 1%    ~     (p=0.151 n=5+5)
_jsonBody-2                  63.3µs ± 3%    64.5µs ± 1%    ~     (p=0.095 n=5+5)
_jsonBodyValidation-2        72.3µs ± 2%    72.4µs ± 2%    ~     (p=1.000 n=5+5)
_outputHeaders-2             35.5µs ± 1%    36.2µs ± 2%    ~     (p=0.056 n=5+5)
_requestResponseMapping-2    60.0µs ± 2%    62.9µs ± 1%  +4.86%  (p=0.008 n=5+5)
_validation-2                66.5µs ± 2%    67.7µs ± 1%    ~     (p=0.222 n=5+5)
_noValidation-2              46.3µs ± 2%    46.7µs ± 1%    ~     (p=0.310 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                37.6µs ± 2%    37.9µs ± 2%    ~     (p=0.310 n=5+5)
_directGzipHead-2            39.2µs ±13%    37.3µs ± 1%    ~     (p=0.056 n=5+5)
_noDirectGzip-2               178µs ± 6%     176µs ± 4%    ~     (p=0.548 n=5+5)
_directGzip_decode-2          543µs ± 2%     571µs ± 4%  +5.05%  (p=0.016 n=5+5)
_noDirectGzip_decode-2        173µs ± 1%     175µs ± 1%    ~     (p=0.056 n=5+5)
_jsonBody-2                  65.8µs ± 1%    68.6µs ± 2%  +4.36%  (p=0.008 n=5+5)
_jsonBodyValidation-2        72.3µs ± 3%    75.6µs ± 5%  +4.62%  (p=0.016 n=5+5)
_outputHeaders-2             38.7µs ±13%    37.6µs ± 3%    ~     (p=1.000 n=5+5)
_requestResponseMapping-2    63.6µs ± 8%    65.2µs ± 1%    ~     (p=0.190 n=5+4)
_validation-2                67.2µs ± 1%    71.3µs ± 1%  +5.98%  (p=0.008 n=5+5)
_noValidation-2              67.2µs ± 2%    71.1µs ± 2%  +5.70%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2               43.5µs ± 2%    46.3µs ±14%  +6.57%  (p=0.008 n=5+5)
_ok-2                        46.0µs ±15%    46.4µs ± 1%    ~     (p=0.151 n=5+5)
_invalidBody-2               61.0µs ± 3%    62.7µs ± 1%  +2.85%  (p=0.016 n=5+5)

name                       old B:rcvd/op  new B:rcvd/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                   639 ± 0%       639 ± 0%    ~     (all equal)
_directGzipHead-2               183 ± 0%       183 ± 0%    ~     (all equal)
_noDirectGzip-2               1.04k ± 0%     1.04k ± 0%    ~     (all equal)
_directGzip_decode-2            639 ± 0%       639 ± 0%    ~     (all equal)
_noDirectGzip_decode-2        1.04k ± 0%     1.04k ± 0%    ~     (all equal)
_jsonBody-2                     208 ± 0%       208 ± 0%    ~     (all equal)
_jsonBodyValidation-2           194 ± 0%       194 ± 0%    ~     (all equal)
_outputHeaders-2                155 ± 0%       155 ± 0%    ~     (all equal)
_requestResponseMapping-2      94.0 ± 0%      94.0 ± 0%    ~     (all equal)
_validation-2                   177 ± 0%       177 ± 0%    ~     (all equal)
_noValidation-2                 177 ± 0%       177 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                   639 ± 0%       639 ± 0%    ~     (all equal)
_directGzipHead-2               183 ± 0%       183 ± 0%    ~     (all equal)
_noDirectGzip-2               1.04k ± 0%     1.04k ± 0%    ~     (all equal)
_directGzip_decode-2            639 ± 0%       639 ± 0%    ~     (all equal)
_noDirectGzip_decode-2        1.04k ± 0%     1.04k ± 0%    ~     (all equal)
_jsonBody-2                     208 ± 0%       208 ± 0%    ~     (all equal)
_jsonBodyValidation-2           194 ± 0%       194 ± 0%    ~     (all equal)
_outputHeaders-2                155 ± 0%       155 ± 0%    ~     (all equal)
_requestResponseMapping-2      94.0 ± 0%      94.0 ± 0%    ~     (all equal)
_validation-2                   177 ± 0%       177 ± 0%    ~     (all equal)
_noValidation-2                 177 ± 0%       177 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                  337 ± 0%       337 ± 0%    ~     (all equal)
_ok-2                           359 ± 0%       359 ± 0%    ~     (p=0.444 n=5+5)
_invalidBody-2                  435 ± 0%       435 ± 0%    ~     (all equal)

name                       old B:sent/op  new B:sent/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                   103 ± 0%       103 ± 0%    ~     (all equal)
_directGzipHead-2               104 ± 0%       104 ± 0%    ~     (all equal)
_noDirectGzip-2                 117 ± 0%       117 ± 0%    ~     (all equal)
_directGzip_decode-2            116 ± 0%       116 ± 0%    ~     (all equal)
_noDirectGzip_decode-2          130 ± 0%       130 ± 0%    ~     (all equal)
_jsonBody-2                     188 ± 0%       188 ± 0%    ~     (all equal)
_jsonBodyValidation-2           192 ± 0%       192 ± 0%    ~     (all equal)
_outputHeaders-2               77.0 ± 0%      77.0 ± 0%    ~     (all equal)
_requestResponseMapping-2       169 ± 0%       169 ± 0%    ~     (all equal)
_validation-2                   170 ± 0%       170 ± 0%    ~     (all equal)
_noValidation-2                 173 ± 0%       173 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                   103 ± 0%       103 ± 0%    ~     (all equal)
_directGzipHead-2               104 ± 0%       104 ± 0%    ~     (all equal)
_noDirectGzip-2                 117 ± 0%       117 ± 0%    ~     (all equal)
_directGzip_decode-2            116 ± 0%       116 ± 0%    ~     (all equal)
_noDirectGzip_decode-2          130 ± 0%       130 ± 0%    ~     (all equal)
_jsonBody-2                     188 ± 0%       188 ± 0%    ~     (all equal)
_jsonBodyValidation-2           192 ± 0%       192 ± 0%    ~     (all equal)
_outputHeaders-2               77.0 ± 0%      77.0 ± 0%    ~     (all equal)
_requestResponseMapping-2       169 ± 0%       169 ± 0%    ~     (all equal)
_validation-2                   170 ± 0%       170 ± 0%    ~     (all equal)
_noValidation-2                 173 ± 0%       173 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                 74.0 ± 0%      74.0 ± 0%    ~     (all equal)
_ok-2                          74.0 ± 0%      74.0 ± 0%    ~     (all equal)
_invalidBody-2                  137 ± 0%       137 ± 0%    ~     (all equal)

name                       old rps        new rps        delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                 26.3k ± 6%     27.2k ± 1%    ~     (p=0.206 n=5+5)
_directGzipHead-2             27.0k ± 7%     26.4k ±12%    ~     (p=0.548 n=5+5)
_noDirectGzip-2               5.76k ± 1%     5.78k ± 1%    ~     (p=0.690 n=5+5)
_directGzip_decode-2          1.84k ± 2%     1.83k ± 3%    ~     (p=0.548 n=5+5)
_noDirectGzip_decode-2        5.81k ± 0%     5.76k ± 1%    ~     (p=0.151 n=5+5)
_jsonBody-2                   15.8k ± 3%     15.5k ± 1%    ~     (p=0.095 n=5+5)
_jsonBodyValidation-2         13.8k ± 2%     13.8k ± 2%    ~     (p=1.000 n=5+5)
_outputHeaders-2              28.2k ± 1%     27.7k ± 2%    ~     (p=0.056 n=5+5)
_requestResponseMapping-2     16.7k ± 2%     15.9k ± 1%  -4.67%  (p=0.008 n=5+5)
_validation-2                 15.1k ± 2%     14.8k ± 1%    ~     (p=0.222 n=5+5)
_noValidation-2               21.6k ± 2%     21.4k ± 1%    ~     (p=0.421 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                 26.6k ± 2%     26.4k ± 2%    ~     (p=0.310 n=5+5)
_directGzipHead-2             25.6k ±12%     26.8k ± 1%    ~     (p=0.056 n=5+5)
_noDirectGzip-2               5.61k ± 6%     5.68k ± 4%    ~     (p=0.548 n=5+5)
_directGzip_decode-2          1.84k ± 2%     1.75k ± 4%  -4.75%  (p=0.016 n=5+5)
_noDirectGzip_decode-2        5.78k ± 1%     5.71k ± 1%    ~     (p=0.056 n=5+5)
_jsonBody-2                   15.2k ± 1%     14.6k ± 2%  -4.17%  (p=0.008 n=5+5)
_jsonBodyValidation-2         13.8k ± 3%     13.2k ± 5%  -4.38%  (p=0.016 n=5+5)
_outputHeaders-2              26.0k ±12%     26.6k ± 3%    ~     (p=1.000 n=5+5)
_requestResponseMapping-2     15.7k ± 8%     15.3k ± 1%    ~     (p=0.190 n=5+4)
_validation-2                 14.9k ± 1%     14.0k ± 1%  -5.65%  (p=0.008 n=5+5)
_noValidation-2               14.9k ± 2%     14.1k ± 2%  -5.38%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                23.0k ± 2%     21.7k ±12%  -5.76%  (p=0.008 n=5+5)
_ok-2                         21.8k ±13%     21.6k ± 1%    ~     (p=0.151 n=5+5)
_invalidBody-2                16.4k ± 3%     15.9k ± 1%  -2.80%  (p=0.016 n=5+5)

name                       old alloc/op   new alloc/op   delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                3.93kB ± 0%    3.93kB ± 0%    ~     (p=0.865 n=5+5)
_directGzipHead-2            3.93kB ± 0%    3.93kB ± 0%    ~     (p=1.000 n=5+5)
_noDirectGzip-2              6.61kB ±15%    6.68kB ±17%    ~     (p=1.000 n=5+5)
_directGzip_decode-2          402kB ± 0%     402kB ± 0%    ~     (p=0.548 n=5+5)
_noDirectGzip_decode-2       5.32kB ± 5%    5.32kB ± 9%    ~     (p=0.841 n=5+5)
_jsonBody-2                  13.2kB ± 0%    13.2kB ± 0%    ~     (p=0.952 n=5+5)
_jsonBodyValidation-2        19.0kB ± 0%    19.0kB ± 0%    ~     (p=0.310 n=5+5)
_outputHeaders-2             3.71kB ± 0%    3.72kB ± 0%    ~     (p=0.111 n=5+4)
_requestResponseMapping-2    16.7kB ± 0%    16.7kB ± 0%    ~     (p=0.762 n=5+5)
_validation-2                16.6kB ± 0%    16.6kB ± 0%    ~     (p=1.000 n=5+5)
_noValidation-2              6.12kB ± 0%    6.12kB ± 0%    ~     (p=0.825 n=5+4)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                3.95kB ± 0%    3.95kB ± 0%    ~     (p=0.802 n=5+5)
_directGzipHead-2            3.94kB ± 0%    3.94kB ± 0%    ~     (p=0.802 n=5+5)
_noDirectGzip-2              6.67kB ±19%    6.65kB ±27%    ~     (p=0.841 n=5+5)
_directGzip_decode-2          402kB ± 0%     402kB ± 0%    ~     (p=0.651 n=5+5)
_noDirectGzip_decode-2       5.37kB ± 4%    5.51kB ± 5%    ~     (p=0.095 n=5+5)
_jsonBody-2                  13.3kB ± 0%    13.3kB ± 0%    ~     (p=0.111 n=5+5)
_jsonBodyValidation-2        19.0kB ± 0%    19.0kB ± 0%    ~     (p=0.738 n=5+5)
_outputHeaders-2             3.71kB ± 0%    3.72kB ± 0%    ~     (p=0.198 n=5+5)
_requestResponseMapping-2    16.7kB ± 0%    16.7kB ± 0%  +0.06%  (p=0.016 n=5+4)
_validation-2                16.6kB ± 0%    16.6kB ± 0%    ~     (p=0.905 n=5+5)
_noValidation-2              16.6kB ± 0%    16.6kB ± 0%    ~     (p=0.071 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2               4.88kB ± 0%    4.89kB ± 0%  +0.07%  (p=0.016 n=5+5)
_ok-2                        4.79kB ± 0%    4.79kB ± 0%    ~     (p=1.000 n=4+4)
_invalidBody-2               8.77kB ± 4%    8.78kB ± 4%    ~     (p=0.730 n=5+5)

name                       old allocs/op  new allocs/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                  42.0 ± 0%      42.0 ± 0%    ~     (all equal)
_directGzipHead-2              42.0 ± 0%      42.0 ± 0%    ~     (all equal)
_noDirectGzip-2                50.0 ± 4%      50.0 ± 4%    ~     (p=1.000 n=5+5)
_directGzip_decode-2            500 ± 0%       500 ± 0%    ~     (p=1.000 n=5+5)
_noDirectGzip_decode-2         50.0 ± 0%      50.0 ± 0%    ~     (all equal)
_jsonBody-2                     127 ± 0%       127 ± 0%    ~     (all equal)
_jsonBodyValidation-2           183 ± 0%       183 ± 0%    ~     (all equal)
_outputHeaders-2               37.0 ± 0%      37.0 ± 0%    ~     (all equal)
_requestResponseMapping-2       123 ± 0%       123 ± 0%    ~     (all equal)
_validation-2                   152 ± 0%       152 ± 0%    ~     (all equal)
_noValidation-2                72.0 ± 0%      72.0 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                  43.0 ± 0%      43.0 ± 0%    ~     (all equal)
_directGzipHead-2              43.0 ± 0%      43.0 ± 0%    ~     (all equal)
_noDirectGzip-2                51.0 ± 4%      51.0 ± 4%    ~     (p=1.000 n=5+5)
_directGzip_decode-2            501 ± 0%       501 ± 0%    ~     (all equal)
_noDirectGzip_decode-2         51.0 ± 0%      51.0 ± 0%    ~     (all equal)
_jsonBody-2                     128 ± 0%       128 ± 0%    ~     (all equal)
_jsonBodyValidation-2           184 ± 0%       184 ± 0%    ~     (all equal)
_outputHeaders-2               37.0 ± 0%      37.0 ± 0%    ~     (all equal)
_requestResponseMapping-2       124 ± 0%       124 ± 0%    ~     (all equal)
_validation-2                   153 ± 0%       153 ± 0%    ~     (all equal)
_noValidation-2                 153 ± 0%       153 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                 54.0 ± 0%      54.0 ± 0%    ~     (all equal)
_ok-2                          50.0 ± 0%      50.0 ± 0%    ~     (all equal)
_invalidBody-2                  100 ± 0%       101 ± 2%    ~     (p=0.556 n=4+5)
```
</details>
<!-- Sticky Pull Request Commentbench-examples -->

Patch:
diff --git a/.github/workflows/bench-examples.yml b/.github/workflows/bench-examples.yml
index 9e37f5e..f962928 100644
--- a/.github/workflows/bench-examples.yml
+++ b/.github/workflows/bench-examples.yml
@@ -76,6 +76,7 @@ jobs:
         run: |
           export REF_NAME=master
           cd __base
+          go mod tidy
           BENCH_COUNT=5 make bench-run-examples
           ls -lah
           pwd
@@ -85,6 +86,7 @@ jobs:
         id: bench
         run: |
           export REF_NAME=new
+          go mod tidy
           BENCH_COUNT=5 make bench-run-examples bench-stat-examples
           OUTPUT=$(make bench-stat-examples)
           echo "${OUTPUT}"
diff --git a/_examples/go.mod b/_examples/go.mod
index 68e44b2..ebc0c4c 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -5,14 +5,15 @@ go 1.18
 replace github.com/swaggest/rest => ../
 
 require (
-	github.com/bool64/dev v0.2.4
+	github.com/bool64/dev v0.2.5
+	github.com/bool64/httpmock v0.1.1
 	github.com/bool64/httptestbench v0.1.1
-	github.com/go-chi/chi/v5 v5.0.5
+	github.com/go-chi/chi/v5 v5.0.7
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/stretchr/testify v1.7.0
 	github.com/swaggest/assertjson v1.6.8
 	github.com/swaggest/jsonschema-go v0.3.24
-	github.com/swaggest/openapi-go v0.2.13
+	github.com/swaggest/openapi-go v0.2.15
 	github.com/swaggest/rest v0.2.3
 	github.com/swaggest/swgui v1.4.2
 	github.com/swaggest/usecase v1.1.0
@@ -21,7 +22,7 @@ require (
 
 require (
 	github.com/andybalholm/brotli v1.0.3 // indirect
-	github.com/bool64/shared v0.1.3 // indirect
+	github.com/bool64/shared v0.1.4 // indirect
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/iancoleman/orderedmap v0.2.0 // indirect
diff --git a/_examples/go.sum b/_examples/go.sum
index 99de2c0..ea3001d 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -9,12 +9,15 @@ github.com/bool64/dev v0.1.25/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2z
 github.com/bool64/dev v0.1.35/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.1.41/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.1.42/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
-github.com/bool64/dev v0.2.4 h1:eXhpZE+A5Ka2Yjnbm+gQmwj3hgxnYklyyZz5cKtgHiM=
-github.com/bool64/dev v0.2.4/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
+github.com/bool64/dev v0.2.5 h1:H0bylghwcjDBBhEwSFTjArEO9Dr8cCaB54QSOF7esOA=
+github.com/bool64/dev v0.2.5/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
+github.com/bool64/httpmock v0.1.1 h1:jpqM0S8efvJfN7Uy5fBUJKu2C640/ZS0yboxpeyVwm0=
+github.com/bool64/httpmock v0.1.1/go.mod h1:Ju7xrs8gVyxANbgIxoxX4Pkj1uHygzPEpGEnfqct+gA=
 github.com/bool64/httptestbench v0.1.1 h1:08112fXc5F1XuTWLdC9lPGB4FSWNACoeUWTeoR0a2qA=
 github.com/bool64/httptestbench v0.1.1/go.mod h1:gSmfFacu85Je3u6ZjmLZsFolcv7cwg1fLNUCzq2UUsM=
-github.com/bool64/shared v0.1.3 h1:gj7XZPYa1flQsCg3q9AIju+W2A1jaexK0fdFu2XtaG0=
 github.com/bool64/shared v0.1.3/go.mod h1:RF1p1Oi29ofgOvinBpetbF5mceOUP3kpMkvLbWOmtm0=
+github.com/bool64/shared v0.1.4 h1:zwtb1dl2QzDa9TJOq2jzDTdb5IPf9XlxTGKN8cySWT0=
+github.com/bool64/shared v0.1.4/go.mod h1:ryGjsnQFh6BnEXClfVlEJrzjwzat7CmA8PNS5E+jPp0=
 github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -23,9 +26,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
-github.com/go-chi/chi/v5 v5.0.5 h1:l3RJ8T8TAqLsXFfah+RA6N4pydMbPwSdvNM+AFWvLUM=
-github.com/go-chi/chi/v5 v5.0.5/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
+github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
 github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -91,8 +93,8 @@ github.com/swaggest/form/v5 v5.0.1 h1:YQH0REX7iMKhtoVPWXREZgbt50VYXNCKK61psnD8Fg
 github.com/swaggest/form/v5 v5.0.1/go.mod h1:vdnaSTze7cxVKhWiCabrfm1YeLwWLpb9P941Gxv4FnA=
 github.com/swaggest/jsonschema-go v0.3.24 h1:vVtFhWQWT9sgsvgMdh0n3O1F0+TnhOWz4+lbz5p5ChY=
 github.com/swaggest/jsonschema-go v0.3.24/go.mod h1:B2ZSqrrlkj21zhywlkh8VnKBmuqUwDv3dLQoPjgHk7M=
-github.com/swaggest/openapi-go v0.2.13 h1:2u3Im5S6BX6GFqux2I035FSXWn6jgWsXY8SNhc0jHE4=
-github.com/swaggest/openapi-go v0.2.13/go.mod h1:Y3+sBULNSPwoUbExcY+0AwjWX0oxFspaBGha7tx/DWk=
+github.com/swaggest/openapi-go v0.2.15 h1:Cp3c0IacJlDEvYYCBzVm3M51bINyI2XCH5RAwulSj/E=
+github.com/swaggest/openapi-go v0.2.15/go.mod h1:3Xd0go1dJkxlSlBHpVV3h76TccououBojV98hhXn6eQ=
 github.com/swaggest/refl v1.0.0/go.mod h1:acYd5x8NNxivp+ZHdRZKJYz66n/qjo3Q9Sa/jAivljQ=
 github.com/swaggest/refl v1.0.1 h1:YQHb7Ic6EMpdUpxQmTWmf/O4IWN6iIErxJNWA7LwyyM=
 github.com/swaggest/refl v1.0.1/go.mod h1:dnx+n9YaI0o+FH+OR2tJZWLABBVIPs9qc4VY9UdrhLE=
diff --git a/go.mod b/go.mod
index 96227a7..1426d7d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,20 +1,33 @@
 module github.com/swaggest/rest
 
-go 1.13
+go 1.17
 
 require (
-	github.com/bool64/dev v0.2.4
-	github.com/bool64/shared v0.1.3
+	github.com/bool64/dev v0.2.5
+	github.com/bool64/httpmock v0.1.1
+	github.com/bool64/shared v0.1.4
 	github.com/cespare/xxhash/v2 v2.1.2
-	github.com/go-chi/chi/v5 v5.0.4
+	github.com/go-chi/chi/v5 v5.0.7
 	github.com/santhosh-tekuri/jsonschema/v3 v3.1.0
 	github.com/stretchr/testify v1.7.0
 	github.com/swaggest/assertjson v1.6.8
 	github.com/swaggest/form/v5 v5.0.1
 	github.com/swaggest/jsonschema-go v0.3.24
-	github.com/swaggest/openapi-go v0.2.13
+	github.com/swaggest/openapi-go v0.2.15
 	github.com/swaggest/refl v1.0.1
 	github.com/swaggest/usecase v1.1.0
+)
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/iancoleman/orderedmap v0.2.0 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/sergi/go-diff v1.2.0 // indirect
+	github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff // indirect
+	github.com/yudai/gojsondiff v1.0.0 // indirect
+	github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
 	golang.org/x/net v0.0.0-20211105192438-b53810dc28af // indirect
 	golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 )
diff --git a/go.sum b/go.sum
index cb147e1..a6b1d02 100644
--- a/go.sum
+++ b/go.sum
@@ -3,10 +3,13 @@ github.com/bool64/dev v0.1.25/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2z
 github.com/bool64/dev v0.1.35/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.1.41/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.1.42/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
-github.com/bool64/dev v0.2.4 h1:eXhpZE+A5Ka2Yjnbm+gQmwj3hgxnYklyyZz5cKtgHiM=
-github.com/bool64/dev v0.2.4/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
-github.com/bool64/shared v0.1.3 h1:gj7XZPYa1flQsCg3q9AIju+W2A1jaexK0fdFu2XtaG0=
+github.com/bool64/dev v0.2.5 h1:H0bylghwcjDBBhEwSFTjArEO9Dr8cCaB54QSOF7esOA=
+github.com/bool64/dev v0.2.5/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
+github.com/bool64/httpmock v0.1.1 h1:jpqM0S8efvJfN7Uy5fBUJKu2C640/ZS0yboxpeyVwm0=
+github.com/bool64/httpmock v0.1.1/go.mod h1:Ju7xrs8gVyxANbgIxoxX4Pkj1uHygzPEpGEnfqct+gA=
 github.com/bool64/shared v0.1.3/go.mod h1:RF1p1Oi29ofgOvinBpetbF5mceOUP3kpMkvLbWOmtm0=
+github.com/bool64/shared v0.1.4 h1:zwtb1dl2QzDa9TJOq2jzDTdb5IPf9XlxTGKN8cySWT0=
+github.com/bool64/shared v0.1.4/go.mod h1:ryGjsnQFh6BnEXClfVlEJrzjwzat7CmA8PNS5E+jPp0=
 github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -15,8 +18,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/go-chi/chi/v5 v5.0.4 h1:5e494iHzsYBiyXQAHHuI4tyJS9M3V84OuX3ufIIGHFo=
-github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
+github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
 github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -24,16 +27,13 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA=
 github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
-github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
 github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -72,8 +72,8 @@ github.com/swaggest/form/v5 v5.0.1 h1:YQH0REX7iMKhtoVPWXREZgbt50VYXNCKK61psnD8Fg
 github.com/swaggest/form/v5 v5.0.1/go.mod h1:vdnaSTze7cxVKhWiCabrfm1YeLwWLpb9P941Gxv4FnA=
 github.com/swaggest/jsonschema-go v0.3.24 h1:vVtFhWQWT9sgsvgMdh0n3O1F0+TnhOWz4+lbz5p5ChY=
 github.com/swaggest/jsonschema-go v0.3.24/go.mod h1:B2ZSqrrlkj21zhywlkh8VnKBmuqUwDv3dLQoPjgHk7M=
-github.com/swaggest/openapi-go v0.2.13 h1:2u3Im5S6BX6GFqux2I035FSXWn6jgWsXY8SNhc0jHE4=
-github.com/swaggest/openapi-go v0.2.13/go.mod h1:Y3+sBULNSPwoUbExcY+0AwjWX0oxFspaBGha7tx/DWk=
+github.com/swaggest/openapi-go v0.2.15 h1:Cp3c0IacJlDEvYYCBzVm3M51bINyI2XCH5RAwulSj/E=
+github.com/swaggest/openapi-go v0.2.15/go.mod h1:3Xd0go1dJkxlSlBHpVV3h76TccououBojV98hhXn6eQ=
 github.com/swaggest/refl v1.0.0/go.mod h1:acYd5x8NNxivp+ZHdRZKJYz66n/qjo3Q9Sa/jAivljQ=
 github.com/swaggest/refl v1.0.1 h1:YQHb7Ic6EMpdUpxQmTWmf/O4IWN6iIErxJNWA7LwyyM=
 github.com/swaggest/refl v1.0.1/go.mod h1:dnx+n9YaI0o+FH+OR2tJZWLABBVIPs9qc4VY9UdrhLE=
@@ -129,14 +129,12 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=


Test patch:
diff --git a/_examples/task-api/internal/infra/nethttp/integration_test.go b/_examples/task-api/internal/infra/nethttp/integration_test.go
index 57cdb3d..9aa7a59 100644
--- a/_examples/task-api/internal/infra/nethttp/integration_test.go
+++ b/_examples/task-api/internal/infra/nethttp/integration_test.go
@@ -5,11 +5,11 @@ import (
 	"net/http/httptest"
 	"testing"
 
+	"github.com/bool64/httpmock"
 	"github.com/stretchr/testify/assert"
 	"github.com/swaggest/rest/_examples/task-api/internal/infra"
 	"github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp"
 	"github.com/swaggest/rest/_examples/task-api/internal/infra/service"
-	"github.com/swaggest/rest/resttest"
 )
 
 func Test_taskLifeSpan(t *testing.T) {
@@ -19,7 +19,7 @@ func Test_taskLifeSpan(t *testing.T) {
 	srv := httptest.NewServer(nethttp.NewRouter(l))
 	defer srv.Close()
 
-	rc := resttest.NewClient(srv.URL)
+	rc := httpmock.NewClient(srv.URL)
 
 	rc.WithMethod(http.MethodPost).WithURI("/dev/tasks").
 		WithContentType("application/json").
diff --git a/resttest/client.go b/resttest/client.go
index 4bb9d14..9d52f47 100644
--- a/resttest/client.go
+++ b/resttest/client.go
@@ -1,525 +1,17 @@
 package resttest
 
 import (
-	"bytes"
-	"context"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"net/http"
-	"sort"
-	"strings"
-	"sync"
-
-	"github.com/swaggest/assertjson"
-	"github.com/swaggest/assertjson/json5"
+	"github.com/bool64/httpmock"
 )
 
 // Client keeps state of expectations.
-type Client struct {
-	ConcurrencyLevel int
-	JSONComparer     assertjson.Comparer
-	OnBodyMismatch   func(received []byte) // Optional, called when received body does not match expected.
-
-	baseURL string
-
-	// Headers are default headers added to all requests, can be overridden by WithHeader.
-	Headers map[string]string
-
-	// Cookies are default cookies added to all requests, can be overridden by WithCookie.
-	Cookies map[string]string
-
-	resp     *http.Response
-	respBody []byte
-
-	reqHeaders map[string]string
-	reqCookies map[string]string
-	reqBody    []byte
-	reqMethod  string
-	reqURI     string
-
-	// reqConcurrency is a number of simultaneous requests to send.
-	reqConcurrency int
-
-	otherRespBody     []byte
-	otherResp         *http.Response
-	otherRespExpected bool
-}
-
-var (
-	errEmptyBody                = errors.New("received empty body")
-	errResponseCardinality      = errors.New("response status cardinality too high")
-	errUnexpectedBody           = errors.New("unexpected body")
-	errUnexpectedResponseStatus = errors.New("unexpected response status")
-	errOperationNotIdempotent   = errors.New("operation is not idempotent")
-	errNoOtherResponses         = errors.New("all responses have same status, no other responses")
-)
-
-const defaultConcurrencyLevel = 10
-
-// NewClient creates client instance, baseURL may be empty if Client.SetBaseURL is used later.
-func NewClient(baseURL string) *Client {
-	c := &Client{
-		baseURL:      baseURL,
-		JSONComparer: assertjson.Comparer{IgnoreDiff: assertjson.IgnoreDiff},
-	}
-
-	c.Reset()
-
-	if baseURL != "" {
-		c.SetBaseURL(baseURL)
-	}
-
-	return c
-}
-
-// SetBaseURL changes baseURL configured with constructor.
-func (c *Client) SetBaseURL(baseURL string) {
-	if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
-		baseURL = "http://" + baseURL
-	}
-
-	c.baseURL = baseURL
-}
-
-// Reset deletes client state.
-func (c *Client) Reset() *Client {
-	c.reqHeaders = map[string]string{}
-	c.reqCookies = map[string]string{}
-
-	c.resp = nil
-	c.respBody = nil
-
-	c.reqMethod = ""
-	c.reqURI = ""
-	c.reqBody = nil
-
-	c.reqConcurrency = 0
-	c.otherResp = nil
-	c.otherRespBody = nil
-	c.otherRespExpected = false
-
-	return c
-}
-
-// WithMethod sets request HTTP method.
-func (c *Client) WithMethod(method string) *Client {
-	c.reqMethod = method
-
-	return c
-}
-
-// WithPath sets request URI path.
-//
-// Deprecated: use WithURI.
-func (c *Client) WithPath(path string) *Client {
-	c.reqURI = path
-
-	return c
-}
-
-// WithURI sets request URI.
-func (c *Client) WithURI(uri string) *Client {
-	c.reqURI = uri
-
-	return c
-}
-
-// WithBody sets request body.
-func (c *Client) WithBody(body []byte) *Client {
-	c.reqBody = body
-
-	return c
-}
-
-// WithContentType sets request content type.
-func (c *Client) WithContentType(contentType string) *Client {
-	c.reqHeaders["Content-Type"] = contentType
-
-	return c
-}
-
-// WithHeader sets request header.
-func (c *Client) WithHeader(key, value string) *Client {
-	c.reqHeaders[http.CanonicalHeaderKey(key)] = value
-
-	return c
-}
-
-// WithCookie sets request cookie.
-func (c *Client) WithCookie(name, value string) *Client {
-	c.reqCookies[name] = value
-
-	return c
-}
-
-func (c *Client) do() (err error) {
-	if c.reqConcurrency < 1 {
-		c.reqConcurrency = 1
-	}
-
-	// A map of responses count by status code.
-	statusCodeCount := make(map[int]int, 2)
-	wg := sync.WaitGroup{}
-	mu := sync.Mutex{}
-	resps := make(map[int]*http.Response, 2)
-	bodies := make(map[int][]byte, 2)
-
-	for i := 0; i < c.reqConcurrency; i++ {
-		wg.Add(1)
-
-		go func() {
-			var er error
-
-			defer func() {
-				if er != nil {
-					mu.Lock()
-					err = er
-					mu.Unlock()
-				}
-
-				wg.Done()
-			}()
-
-			resp, er := c.doOnce()
-			if er != nil {
-				return
-			}
-
-			body, er := ioutil.ReadAll(resp.Body)
-			if er != nil {
-				return
-			}
-
-			er = resp.Body.Close()
-			if er != nil {
-				return
-			}
-
-			mu.Lock()
-			if _, ok := statusCodeCount[resp.StatusCode]; !ok {
-				resps[resp.StatusCode] = resp
-				bodies[resp.StatusCode] = body
-				statusCodeCount[resp.StatusCode] = 1
-			} else {
-				statusCodeCount[resp.StatusCode]++
-			}
-			mu.Unlock()
-		}()
-	}
-	wg.Wait()
-
-	if err != nil {
-		return err
-	}
-
-	return c.checkResponses(statusCodeCount, bodies, resps)
-}
-
-// CheckResponses checks if responses qualify idempotence criteria.
-//
-// Operation is considered idempotent in one of two cases:
-//  * all responses have same status code (e.g. GET /resource: all 200 OK),
-//  * all responses but one have same status code (e.g. POST /resource: one 200 OK, many 409 Conflict).
-//
-// Any other case is considered an idempotence violation.
-func (c *Client) checkResponses(
-	statusCodeCount map[int]int,
-	bodies map[int][]byte,
-	resps map[int]*http.Response,
-) error {
-	var (
-		statusCode      int
-		otherStatusCode int
-	)
-
-	switch {
-	case len(statusCodeCount) == 1:
-		for code := range statusCodeCount {
-			statusCode = code
-
-			break
-		}
-	case len(statusCodeCount) > 1:
-		for code, cnt := range statusCodeCount {
-			if cnt == 1 {
-				statusCode = code
-			} else {
-				otherStatusCode = code
-			}
-		}
-	default:
-		return fmt.Errorf("%w: %v", errResponseCardinality, statusCodeCount)
-	}
-
-	if statusCode == 0 {
-		responses := ""
-		for c, b := range bodies {
-			responses += fmt.Sprintf("\nstatus %d with %d responses, sample body: %s",
-				c, statusCodeCount[c], strings.Trim(string(b), "\n"))
-		}
-
-		return fmt.Errorf("%w: %v", errOperationNotIdempotent, responses)
-	}
-
-	c.resp = resps[statusCode]
-	c.respBody = bodies[statusCode]
-
-	if otherStatusCode != 0 {
-		c.otherResp = resps[otherStatusCode]
-		c.otherRespBody = bodies[otherStatusCode]
-	}
-
-	return nil
-}
-
-func (c *Client) doOnce() (*http.Response, error) {
-	var reqBody io.Reader
-	if len(c.reqBody) > 0 {
-		reqBody = bytes.NewBuffer(c.reqBody)
-	}
-
-	req, err := http.NewRequestWithContext(context.Background(), c.reqMethod, c.baseURL+c.reqURI, reqBody)
-	if err != nil {
-		return nil, err
-	}
-
-	for k, v := range c.Headers {
-		req.Header.Set(k, v)
-	}
-
-	for k, v := range c.reqHeaders {
-		req.Header.Set(k, v)
-	}
-
-	cookies := make([]http.Cookie, 0, len(c.Cookies)+len(c.reqCookies))
-
-	for n, v := range c.Cookies {
-		if _, found := c.reqCookies[n]; found {
-			continue
-		}
-
-		cookies = append(cookies, http.Cookie{Name: n, Value: v})
-	}
-
-	for n, v := range c.reqCookies {
-		cookies = append(cookies, http.Cookie{Name: n, Value: v})
-	}
-
-	sort.Slice(cookies, func(i, j int) bool {
-		return cookies[i].Name < cookies[j].Name
-	})
-
-	for _, v := range cookies {
-		v := v
-		req.AddCookie(&v)
-	}
-
-	return http.DefaultTransport.RoundTrip(req)
-}
-
-// ExpectResponseStatus sets expected response status code.
-func (c *Client) ExpectResponseStatus(statusCode int) error {
-	if c.resp == nil {
-		err := c.do()
-		if err != nil {
-			return err
-		}
-	}
-
-	return c.assertResponseCode(statusCode, c.resp)
-}
-
-// ExpectResponseHeader asserts expected response header value.
-func (c *Client) ExpectResponseHeader(key, value string) error {
-	if c.resp == nil {
-		err := c.do()
-		if err != nil {
-			return err
-		}
-	}
-
-	return c.assertResponseHeader(key, value, c.resp)
-}
-
-// CheckUnexpectedOtherResponses fails if other responses were present, but not expected with
-// ExpectOther* functions.
-//
-// Does not affect single (non-concurrent) calls.
-func (c *Client) CheckUnexpectedOtherResponses() error {
-	if c.otherRespExpected || c.otherResp == nil {
-		return nil
-	}
-
-	return c.assertResponseCode(c.resp.StatusCode, c.otherResp)
-}
-
-// ExpectNoOtherResponses sets expectation for only one response status to be received  during concurrent
-// calling.
-//
-// Does not affect single (non-concurrent) calls.
-func (c *Client) ExpectNoOtherResponses() error {
-	if c.resp == nil {
-		if err := c.do(); err != nil {
-			return err
-		}
-	}
-
-	if c.otherResp != nil {
-		return c.assertResponseCode(c.resp.StatusCode, c.otherResp)
-	}
-
-	return nil
-}
-
-// ExpectOtherResponsesStatus sets expectation for response status to be received one or more times during concurrent
-// calling.
-//
-// For example, it may describe "Not Found" response on multiple DELETE or "Conflict" response on multiple POST.
-// Does not affect single (non-concurrent) calls.
-func (c *Client) ExpectOtherResponsesStatus(statusCode int) error {
-	c.otherRespExpected = true
-
-	if c.resp == nil {
-		if err := c.do(); err != nil {
-			return err
-		}
-	}
-
-	if c.otherResp == nil {
-		return errNoOtherResponses
-	}
-
-	return c.assertResponseCode(statusCode, c.otherResp)
-}
-
-// ExpectOtherResponsesHeader sets expectation for response header value to be received one or more times during
-// concurrent calling.
-func (c *Client) ExpectOtherResponsesHeader(key, value string) error {
-	c.otherRespExpected = true
-
-	if c.resp == nil {
-		if err := c.do(); err != nil {
-			return err
-		}
-	}
-
-	if c.otherResp == nil {
-		return errNoOtherResponses
-	}
-
-	return c.assertResponseHeader(key, value, c.otherResp)
-}
-
-func (c *Client) assertResponseCode(statusCode int, resp *http.Response) error {
-	if resp.StatusCode != statusCode {
-		return fmt.Errorf("%w, expected: %d (%s), received: %d (%s)", errUnexpectedResponseStatus,
-			statusCode, http.StatusText(statusCode), resp.StatusCode, http.StatusText(resp.StatusCode))
-	}
-
-	return nil
-}
-
-func (c *Client) assertResponseHeader(key, value string, resp *http.Response) error {
-	expected, err := json.Marshal(value)
-	if err != nil {
-		return err
-	}
-
-	received, err := json.Marshal(resp.Header.Get(key))
-	if err != nil {
-		return err
-	}
-
-	return c.JSONComparer.FailNotEqual(expected, received)
-}
-
-// ExpectResponseBody sets expectation for response body to be received.
 //
-// In concurrent mode such response mush be met only once or for all calls.
-func (c *Client) ExpectResponseBody(body []byte) error {
-	if c.resp == nil {
-		err := c.do()
-		if err != nil {
-			return err
-		}
-	}
-
-	return c.checkBody(body, c.respBody)
-}
+// Deprecated: please use httpmock.Client.
+type Client = httpmock.Client
 
-// ExpectOtherResponsesBody sets expectation for response body to be received one or more times during concurrent
-// calling.
+// NewClient creates client instance, baseURL may be empty if Client.SetBaseURL is used later.
 //
-// For example, it may describe "Not Found" response on multiple DELETE or "Conflict" response on multiple POST.
-// Does not affect single (non-concurrent) calls.
-func (c *Client) ExpectOtherResponsesBody(body []byte) error {
-	c.otherRespExpected = true
-
-	if c.resp == nil {
-		err := c.do()
-		if err != nil {
-			return err
-		}
-	}
-
-	if c.otherResp == nil {
-		return errNoOtherResponses
-	}
-
-	return c.checkBody(body, c.otherRespBody)
-}
-
-func (c *Client) checkBody(expected, received []byte) (err error) {
-	if len(received) == 0 {
-		if len(expected) == 0 {
-			return nil
-		}
-
-		return errEmptyBody
-	}
-
-	defer func() {
-		if err != nil && c.OnBodyMismatch != nil {
-			c.OnBodyMismatch(received)
-		}
-	}()
-
-	if json5.Valid(expected) && json5.Valid(received) {
-		expected, err := json5.Downgrade(expected)
-		if err != nil {
-			return err
-		}
-
-		err = c.JSONComparer.FailNotEqual(expected, received)
-		if err != nil {
-			recCompact, cerr := assertjson.MarshalIndentCompact(json.RawMessage(received), "", " ", 100)
-			if cerr == nil {
-				received = recCompact
-			}
-
-			return fmt.Errorf("%w\nreceived:\n%s ", err, string(received))
-		}
-
-		return nil
-	}
-
-	if !bytes.Equal(expected, received) {
-		return fmt.Errorf("%w, expected: %s, received: %s",
-			errUnexpectedBody, string(expected), string(received))
-	}
-
-	return nil
-}
-
-// Concurrently enables concurrent calls to idempotent endpoint.
-func (c *Client) Concurrently() *Client {
-	c.reqConcurrency = c.ConcurrencyLevel
-	if c.reqConcurrency == 0 {
-		c.reqConcurrency = defaultConcurrencyLevel
-	}
-
-	return c
+// Deprecated: please use httpmock.NewClient.
+func NewClient(baseURL string) *Client {
+	return httpmock.NewClient(baseURL)
 }
diff --git a/resttest/client_test.go b/resttest/client_test.go
index 5f681d2..702b162 100644
--- a/resttest/client_test.go
+++ b/resttest/client_test.go
@@ -7,9 +7,9 @@ import (
 	"sync/atomic"
 	"testing"
 
+	"github.com/bool64/httpmock"
 	"github.com/bool64/shared"
 	"github.com/stretchr/testify/assert"
-	"github.com/swaggest/rest/resttest"
 )
 
 func TestNewClient(t *testing.T) {
@@ -52,7 +52,7 @@ func TestNewClient(t *testing.T) {
 
 	vars := &shared.Vars{}
 
-	c := resttest.NewClient(srv.URL)
+	c := httpmock.NewClient(srv.URL)
 	c.JSONComparer.Vars = vars
 	c.ConcurrencyLevel = 50
 	c.Headers = map[string]string{
@@ -93,7 +93,7 @@ func TestNewClient_failedExpectation(t *testing.T) {
 		assert.NoError(t, err)
 	}))
 	defer srv.Close()
-	c := resttest.NewClient(srv.URL)
+	c := httpmock.NewClient(srv.URL)
 
 	c.OnBodyMismatch = func(received []byte) {
 		assert.Equal(t, `{"bar":"foo"}`, string(received))
diff --git a/resttest/doc.go b/resttest/doc.go
index bde58ba..f2ca23f 100644
--- a/resttest/doc.go
+++ b/resttest/doc.go
@@ -1,2 +1,4 @@
 // Package resttest provides utilities to test REST API.
+//
+// Deprecated: please use github.com/bool64/httpmock.
 package resttest
diff --git a/resttest/example_test.go b/resttest/example_test.go
index 76a9807..466aa4f 100644
--- a/resttest/example_test.go
+++ b/resttest/example_test.go
@@ -4,19 +4,19 @@ import (
 	"fmt"
 	"net/http"
 
-	"github.com/swaggest/rest/resttest"
+	"github.com/bool64/httpmock"
 )
 
 func ExampleNewClient() {
 	// Prepare server mock.
-	sm, url := resttest.NewServerMock()
+	sm, url := httpmock.NewServer()
 	defer sm.Close()
 
 	// This example shows Client and ServerMock working together for sake of portability.
 	// In real-world scenarios Client would complement real server or ServerMock would complement real HTTP client.
 
 	// Set successful expectation for first request out of concurrent batch.
-	exp := resttest.Expectation{
+	exp := httpmock.Expectation{
 		Method:     http.MethodPost,
 		RequestURI: "/foo?q=1",
 		RequestHeader: map[string]string{
@@ -37,7 +37,7 @@ func ExampleNewClient() {
 	sm.Expect(exp)
 
 	// Prepare client request.
-	c := resttest.NewClient(url)
+	c := httpmock.NewClient(url)
 	c.ConcurrencyLevel = 50
 	c.Headers = map[string]string{
 		"X-Header": "abc",
diff --git a/resttest/server.go b/resttest/server.go
index e48f285..b013792 100644
--- a/resttest/server.go
+++ b/resttest/server.go
@@ -1,364 +1,20 @@
 package resttest
 
 import (
-	"bytes"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io/ioutil"
-	"net/http"
-	"net/http/httptest"
-	"sync"
-
-	"github.com/swaggest/assertjson"
-	"github.com/swaggest/assertjson/json5"
+	"github.com/bool64/httpmock"
 )
 
 // Expectation describes expected request and defines response.
-type Expectation struct {
-	Method        string
-	RequestURI    string
-	RequestHeader map[string]string
-	RequestCookie map[string]string
-	RequestBody   []byte
-
-	Status         int
-	ResponseHeader map[string]string
-	ResponseBody   []byte
-
-	// Unlimited enables reusing of this expectation unlimited number of times.
-	Unlimited bool
-	// Repeated defines how many times this expectation should be used.
-	Repeated int
-}
+//
+// Deprecated: please use httpmock.Expectation.
+type Expectation = httpmock.Expectation
 
 // ServerMock serves predefined response for predefined request.
-type ServerMock struct {
-	// OnError is called on expectations mismatch or internal errors.
-	OnError func(err error)
-
-	// ErrorResponder allows custom failure responses.
-	ErrorResponder func(rw http.ResponseWriter, err error)
-
-	// DefaultResponseHeaders are added to every response to an expected request.
-	DefaultResponseHeaders map[string]string
-
-	// JSONComparer controls JSON equality check.
-	JSONComparer assertjson.Comparer
-
-	// OnBodyMismatch is called when received body does not match expected, optional.
-	OnBodyMismatch func(received []byte)
-
-	mu           sync.Mutex
-	server       *httptest.Server
-	expectations []Expectation
-	async        []Expectation
-}
+type ServerMock = httpmock.Server
 
 // NewServerMock creates mocked server.
-func NewServerMock() (*ServerMock, string) {
-	m := ServerMock{}
-	m.server = httptest.NewServer(&m)
-	m.JSONComparer = assertjson.Comparer{IgnoreDiff: assertjson.IgnoreDiff}
-
-	return &m, m.server.URL
-}
-
-// Expect adds expected operation.
-func (sm *ServerMock) Expect(e Expectation) {
-	sm.mu.Lock()
-	defer sm.mu.Unlock()
-
-	sm.expectations = append(sm.expectations, e)
-}
-
-// ExpectAsync sets non-sequential expectation.
 //
-// Asynchronous expectations are checked for every incoming request,
-// first match is used for response.
-// If there are no matches, regular (sequential expectations are used).
-func (sm *ServerMock) ExpectAsync(e Expectation) {
-	sm.mu.Lock()
-	defer sm.mu.Unlock()
-
-	sm.async = append(sm.async, e)
-}
-
-// Close closes mocked server.
-func (sm *ServerMock) Close() {
-	sm.server.Close()
-}
-
-func (sm *ServerMock) prepareBody(data []byte) ([]byte, error) {
-	if sm.JSONComparer.Vars == nil {
-		return data, nil
-	}
-
-	if !json.Valid(data) {
-		return data, nil
-	}
-
-	for k, v := range sm.JSONComparer.Vars.GetAll() {
-		j, err := json.Marshal(v)
-		if err != nil {
-			return nil, err
-		}
-
-		data = bytes.ReplaceAll(data, []byte(`"`+k+`"`), j)
-	}
-
-	return data, nil
-}
-
-func (sm *ServerMock) writeResponse(rw http.ResponseWriter, expectation Expectation) bool {
-	var err error
-
-	if sm.DefaultResponseHeaders != nil {
-		for k, v := range sm.DefaultResponseHeaders {
-			rw.Header().Set(k, v)
-		}
-	}
-
-	if expectation.ResponseHeader != nil {
-		for k, v := range expectation.ResponseHeader {
-			rw.Header().Set(k, v)
-		}
-	}
-
-	if expectation.Status == 0 {
-		expectation.Status = http.StatusOK
-	}
-
-	rw.WriteHeader(expectation.Status)
-
-	expectation.ResponseBody, err = sm.prepareBody(expectation.ResponseBody)
-	if sm.checkFail(rw, err) {
-		return false
-	}
-
-	_, err = rw.Write(expectation.ResponseBody)
-
-	return !sm.checkFail(rw, err)
-}
-
-func (sm *ServerMock) checkAsync(rw http.ResponseWriter, req *http.Request) bool {
-	for i, expectation := range sm.async {
-		if err := sm.checkRequest(req, expectation); err != nil {
-			continue
-		}
-
-		if !sm.writeResponse(rw, expectation) {
-			return true
-		}
-
-		if expectation.Unlimited {
-			return true
-		}
-
-		if expectation.Repeated > 0 {
-			expectation.Repeated--
-			sm.async[i] = expectation
-
-			if expectation.Repeated > 0 {
-				return true
-			}
-		}
-
-		// Deleting expectation.
-		sm.async[i] = sm.async[len(sm.async)-1]
-		sm.async = sm.async[:len(sm.async)-1]
-
-		return true
-	}
-
-	return false
-}
-
-// ServeHTTP asserts request expectations and serves mocked response.
-func (sm *ServerMock) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
-	sm.mu.Lock()
-	defer sm.mu.Unlock()
-
-	if sm.checkAsync(rw, req) {
-		return
-	}
-
-	if len(sm.expectations) == 0 {
-		body, err := ioutil.ReadAll(req.Body)
-		if err == nil && len(body) > 0 {
-			if sm.OnBodyMismatch != nil {
-				sm.OnBodyMismatch(body)
-			}
-
-			sm.checkFail(rw, fmt.Errorf("unexpected request received: %s %s, body:\n%s", req.Method,
-				req.RequestURI, string(body)))
-		} else {
-			sm.checkFail(rw, fmt.Errorf("unexpected request received: %s %s", req.Method, req.RequestURI))
-		}
-
-		return
-	}
-
-	expectation := sm.expectations[0]
-
-	err := sm.checkRequest(req, expectation)
-	if sm.checkFail(rw, err) {
-		return
-	}
-
-	if !sm.writeResponse(rw, expectation) {
-		return
-	}
-
-	if expectation.Unlimited {
-		return
-	}
-
-	if expectation.Repeated > 0 {
-		expectation.Repeated--
-		sm.expectations[0] = expectation
-
-		if expectation.Repeated > 0 {
-			return
-		}
-	}
-
-	sm.expectations = sm.expectations[1:]
-}
-
-func (sm *ServerMock) checkBody(expected, received []byte) (err error) {
-	defer func() {
-		if err != nil && sm.OnBodyMismatch != nil {
-			sm.OnBodyMismatch(received)
-		}
-	}()
-
-	if !json5.Valid(expected) || !json5.Valid(received) {
-		if !bytes.Equal(expected, received) {
-			return errors.New("unexpected request body")
-		}
-	} else {
-		// Performing JSON comparison for JSON payloads and binary comparison otherwise.
-		expected, err := json5.Downgrade(expected)
-		if err != nil {
-			return err
-		}
-
-		if err = sm.JSONComparer.FailNotEqual(expected, received); err != nil {
-			return fmt.Errorf("unexpected request body: %w", err)
-		}
-	}
-
-	return nil
-}
-
-func (sm *ServerMock) checkRequest(req *http.Request, expectation Expectation) error {
-	if expectation.Method != "" && expectation.Method != req.Method {
-		return fmt.Errorf("method %q expected, %q received", expectation.Method, req.Method)
-	}
-
-	if expectation.RequestURI != "" && expectation.RequestURI != req.RequestURI {
-		return fmt.Errorf("request uri %q expected, %q received", expectation.RequestURI, req.RequestURI)
-	}
-
-	for k, v := range expectation.RequestHeader {
-		if req.Header.Get(k) != v {
-			return fmt.Errorf("header %q with value %q expected, %q received", k, v, req.Header.Get(k))
-		}
-	}
-
-	for n, v := range expectation.RequestCookie {
-		c, err := req.Cookie(n)
-		if err != nil {
-			return fmt.Errorf("failed to find cookie %q with value %q: %w", n, v, err)
-		}
-
-		if c.Value != v {
-			return fmt.Errorf("header %q with value %q expected, %q received", n, v, c.Value)
-		}
-	}
-
-	reqBody, err := ioutil.ReadAll(req.Body)
-	if err != nil {
-		return err
-	}
-
-	if expectation.RequestBody == nil {
-		return nil
-	}
-
-	return sm.checkBody(expectation.RequestBody, reqBody)
-}
-
-func (sm *ServerMock) checkFail(rw http.ResponseWriter, err error) bool {
-	if err == nil {
-		return false
-	}
-
-	if sm.OnError != nil {
-		sm.OnError(err)
-	}
-
-	if sm.ErrorResponder != nil {
-		sm.ErrorResponder(rw, err)
-
-		return true
-	}
-
-	rw.WriteHeader(http.StatusInternalServerError)
-
-	_, err = rw.Write([]byte(err.Error()))
-	if err != nil && sm.OnError != nil {
-		sm.OnError(err)
-	}
-
-	return true
-}
-
-// ResetExpectations discards all expectation to reset the state of mock.
-func (sm *ServerMock) ResetExpectations() {
-	sm.mu.Lock()
-	defer sm.mu.Unlock()
-
-	sm.expectations = nil
-	sm.async = nil
-}
-
-// ExpectationsWereMet checks whether all queued expectations
-// were met in order. If any of them was not met - an error is returned.
-func (sm *ServerMock) ExpectationsWereMet() error {
-	sm.mu.Lock()
-	defer sm.mu.Unlock()
-
-	var unmet string
-
-	for _, e := range sm.expectations {
-		if e.Unlimited {
-			continue
-		}
-
-		if e.Method != "" || e.RequestURI != "" {
-			unmet += ", " + e.Method + " " + e.RequestURI
-		} else {
-			unmet += ", response " + string(e.ResponseBody)
-		}
-	}
-
-	for _, e := range sm.async {
-		if e.Unlimited {
-			continue
-		}
-
-		if e.Method != "" || e.RequestURI != "" {
-			unmet += ", " + e.Method + " " + e.RequestURI
-		} else {
-			unmet += ", response " + string(e.ResponseBody)
-		}
-	}
-
-	if unmet != "" {
-		return errors.New("there are remaining expectations that were not met: " + unmet[2:])
-	}
-
-	return nil
+// Deprecated: please use httpmock.NewServer.
+func NewServerMock() (*ServerMock, string) {
+	return httpmock.NewServer()
 }
diff --git a/resttest/server_test.go b/resttest/server_test.go
index 7ebb91a..3d4d6aa 100644
--- a/resttest/server_test.go
+++ b/resttest/server_test.go
@@ -9,13 +9,13 @@ import (
 	"sync"
 	"testing"
 
+	"github.com/bool64/httpmock"
 	"github.com/bool64/shared"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"github.com/swaggest/rest/resttest"
 )
 
-func assertRoundTrip(t *testing.T, baseURL string, expectation resttest.Expectation) {
+func assertRoundTrip(t *testing.T, baseURL string, expectation httpmock.Expectation) {
 	t.Helper()
 
 	var bodyReader io.Reader
@@ -63,7 +63,7 @@ func assertRoundTrip(t *testing.T, baseURL string, expectation resttest.Expectat
 
 func TestServerMock_ServeHTTP(t *testing.T) {
 	// Creating REST service mock.
-	mock, baseURL := resttest.NewServerMock()
+	mock, baseURL := httpmock.NewServer()
 	defer mock.Close()
 
 	mock.OnBodyMismatch = func(received []byte) {
@@ -75,14 +75,14 @@ func TestServerMock_ServeHTTP(t *testing.T) {
 	}
 
 	// Requesting mock without expectations fails.
-	assertRoundTrip(t, baseURL, resttest.Expectation{
+	assertRoundTrip(t, baseURL, httpmock.Expectation{
 		RequestURI:   "/test?test=test",
 		Status:       http.StatusInternalServerError,
 		ResponseBody: []byte("unexpected request received: GET /test?test=test"),
 	})
 
 	// Requesting mock without expectations fails.
-	assertRoundTrip(t, baseURL, resttest.Expectation{
+	assertRoundTrip(t, baseURL, httpmock.Expectation{
 		RequestURI:   "/test?test=test",
 		Status:       http.StatusInternalServerError,
 		RequestBody:  []byte(`{"foo":"bar"}`),
@@ -90,7 +90,7 @@ func TestServerMock_ServeHTTP(t *testing.T) {
 	})
 
 	// Setting expectations for first request.
-	exp1 := resttest.Expectation{
+	exp1 := httpmock.Expectation{
 		Method:        http.MethodPost,
 		RequestURI:    "/test?test=test",
 		RequestHeader: map[string]string{"Authorization": "Bearer token"},
@@ -103,7 +103,7 @@ func TestServerMock_ServeHTTP(t *testing.T) {
 	mock.Expect(exp1)
 
 	// Setting expectations for second request.
-	exp2 := resttest.Expectation{
+	exp2 := httpmock.Expectation{
 		Method:      http.MethodPost,
 		RequestURI:  "/test?test=test",
 		RequestBody: []byte(`not a JSON`),
@@ -129,7 +129,7 @@ func TestServerMock_ServeHTTP(t *testing.T) {
 	assert.NoError(t, mock.ExpectationsWereMet())
 
 	// Requesting mock without expectations fails.
-	assertRoundTrip(t, baseURL, resttest.Expectation{
+	assertRoundTrip(t, baseURL, httpmock.Expectation{
 		RequestURI:   "/test?test=test",
 		Status:       http.StatusInternalServerError,
 		ResponseBody: []byte("unexpected request received: GET /test?test=test"),
@@ -138,7 +138,7 @@ func TestServerMock_ServeHTTP(t *testing.T) {
 
 func TestServerMock_ServeHTTP_error(t *testing.T) {
 	// Creating REST service mock.
-	mock, baseURL := resttest.NewServerMock()
+	mock, baseURL := httpmock.NewServer()
 	defer mock.Close()
 
 	mock.OnBodyMismatch = func(received []byte) {
@@ -146,7 +146,7 @@ func TestServerMock_ServeHTTP_error(t *testing.T) {
 	}
 
 	// Setting expectations for first request.
-	mock.Expect(resttest.Expectation{
+	mock.Expect(httpmock.Expectation{
 		Method:        http.MethodPost,
 		RequestURI:    "/test?test=test",
 		RequestHeader: map[string]string{"X-Foo": "bar"},
@@ -221,14 +221,14 @@ func TestServerMock_ServeHTTP_error(t *testing.T) {
 
 func TestServerMock_ServeHTTP_concurrency(t *testing.T) {
 	// Creating REST service mock.
-	mock, url := resttest.NewServerMock()
+	mock, url := httpmock.NewServer()
 	defer mock.Close()
 
 	n := 50
 
 	for i := 0; i < n; i++ {
 		// Setting expectations for first request.
-		mock.Expect(resttest.Expectation{
+		mock.Expect(httpmock.Expectation{
 			Method:       http.MethodGet,
 			RequestURI:   "/test?test=test",
 			ResponseBody: []byte("body"),
@@ -265,16 +265,16 @@ func TestServerMock_ServeHTTP_concurrency(t *testing.T) {
 
 func TestServerMock_ResetExpectations(t *testing.T) {
 	// Creating REST service mock.
-	mock, _ := resttest.NewServerMock()
+	mock, _ := httpmock.NewServer()
 	defer mock.Close()
 
-	mock.Expect(resttest.Expectation{
+	mock.Expect(httpmock.Expectation{
 		Method:       http.MethodGet,
 		RequestURI:   "/test?test=test",
 		ResponseBody: []byte("body"),
 	})
 
-	mock.ExpectAsync(resttest.Expectation{
+	mock.ExpectAsync(httpmock.Expectation{
 		Method:       http.MethodGet,
 		RequestURI:   "/test-async?test=test",
 		ResponseBody: []byte("body"),
@@ -286,9 +286,9 @@ func TestServerMock_ResetExpectations(t *testing.T) {
 }
 
 func TestServerMock_vars(t *testing.T) {
-	sm, url := resttest.NewServerMock()
+	sm, url := httpmock.NewServer()
 	sm.JSONComparer.Vars = &shared.Vars{}
-	sm.Expect(resttest.Expectation{
+	sm.Expect(httpmock.Expectation{
 		Method:       http.MethodGet,
 		RequestURI:   "/",
 		RequestBody:  []byte(`{"foo":"bar","dyn":"$var1"}`),
@@ -310,18 +310,18 @@ func TestServerMock_vars(t *testing.T) {
 }
 
 func TestServerMock_ExpectAsync(t *testing.T) {
-	sm, url := resttest.NewServerMock()
-	sm.Expect(resttest.Expectation{
+	sm, url := httpmock.NewServer()
+	sm.Expect(httpmock.Expectation{
 		Method:       http.MethodGet,
 		RequestURI:   "/",
 		ResponseBody: []byte(`{"bar":"foo"}`),
 	})
-	sm.ExpectAsync(resttest.Expectation{
+	sm.ExpectAsync(httpmock.Expectation{
 		Method:       http.MethodGet,
 		RequestURI:   "/async1",
 		ResponseBody: []byte(`{"bar":"async1"}`),
 	})
-	sm.ExpectAsync(resttest.Expectation{
+	sm.ExpectAsync(httpmock.Expectation{
 		Method:       http.MethodGet,
 		RequestURI:   "/async2",
 		ResponseBody: []byte(`{"bar":"async2"}`),