├── .circleci
└── config.yml
├── .formatter.exs
├── .gitignore
├── BENCHMARK.md
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── assets
├── mojito-full.png
└── mojito.png
├── config
├── config.exs
└── test.exs
├── lib
├── mojito.ex
└── mojito
│ ├── application.ex
│ ├── base.ex
│ ├── config.ex
│ ├── conn.ex
│ ├── conn_server.ex
│ ├── error.ex
│ ├── headers.ex
│ ├── pool.ex
│ ├── pool
│ ├── poolboy.ex
│ └── poolboy
│ │ ├── manager.ex
│ │ └── single.ex
│ ├── request.ex
│ ├── request
│ └── single.ex
│ ├── response.ex
│ ├── telemetry.ex
│ └── utils.ex
├── mix.exs
├── mix.lock
└── test
├── headers_test.exs
├── mojito_sync_test.exs
├── mojito_test.exs
├── pool
├── poolboy
│ ├── manager_test.exs
│ └── single_test.exs
└── poolboy_test.exs
├── support
├── cert.pem
├── key.pem
└── mojito_test_server.ex
└── test_helper.exs
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Elixir CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-elixir/ for more details
4 | version: 2
5 | jobs:
6 | build:
7 | docker:
8 | - image: circleci/elixir:1.11
9 | working_directory: ~/app
10 | steps:
11 | - run: mix local.hex --force
12 | - run: mix local.rebar --force
13 |
14 | - checkout
15 | - run: mix format --check-formatted
16 |
17 | # Read about caching dependencies: https://circleci.com/docs/2.0/caching/
18 | - restore_cache: # restores saved mix cache
19 | keys: # list of cache keys, in decreasing specificity
20 | - v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }}
21 | - v1-mix-cache-{{ .Branch }}
22 | - v1-mix-cache
23 | - restore_cache: # restores saved build cache
24 | keys:
25 | - v1-build-cache-{{ .Branch }}
26 | - v1-build-cache
27 | - run: mix do deps.get, compile # get updated dependencies & compile them
28 | - save_cache: # generate and store mix cache
29 | key: v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }}
30 | paths: "deps"
31 | - save_cache: # don't forget to save a *build* cache, too
32 | key: v1-build-cache-{{ .Branch }}
33 | paths: "_build"
34 |
35 | - run: mix test
36 |
37 | - store_test_results: # upload junit test results for display in Test Summary
38 | # Read more: https://circleci.com/docs/2.0/collect-test-data/
39 | path: _build/test/lib/mojito # Replace with the name of your :app
40 |
--------------------------------------------------------------------------------
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
4 | line_length: 80,
5 | trailing_comma: true
6 | ]
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | mojito-*.tar
24 |
25 | # Temporary files, for example, from tests.
26 | /tmp/
27 |
--------------------------------------------------------------------------------
/BENCHMARK.md:
--------------------------------------------------------------------------------
1 | # Benchmarks
2 |
3 | These benchmarks were conducted using
4 | [gamache/httpc_bench](https://github.com/gamache/httpc_bench).
5 |
6 | GET requests lasting 10 ms are performed.
7 |
8 | Clients ran on AWS m5.4xlarge (16 vCPUs, 64 GB RAM) running Ubuntu 18.04.
9 |
10 | The server ran on a separate m5.4xlarge, configured identically.
11 |
12 | Clients:
13 |
14 | * Mojito 0.3.0 (Mint 0.2.1)
15 | * Buoy `b65c06f`
16 | * MachineGun 0.1.5 (Gun 1.3.0)
17 | * Dlhttpc `1072652`
18 | * Hackney 1.15.1
19 | * Ibrowse 4.4.1
20 | * Httpc (OTP 21.2)
21 | * Mint 0.2.1, without pooling or keepalive, as a reference client.
22 |
23 | ## Current results (2019-05-15)
24 |
25 | This benchmark was run after optimizing the servers for high-concurrency
26 | tasks by executing `ulimit -n 12000000` to increase the allowable number
27 | of open files. This configuration is very different from the default
28 | `ulimit -n 1024` on Ubuntu systems.
29 |
30 | The enhanced configuration showed that Buoy and Gun really shine when
31 | they are allowed to stretch their legs. Mojito performs well but has
32 | catch-up work to do! Likely areas of improvement are in pipelining (we
33 | use none, currently) and concurrency-friendly connection checkout in a
34 | similar manner to Shackle or Dispcount.
35 |
36 |
37 | Client | Pool Count | Pool Size | Concurrency | Req/sec | Error % |
38 | Client | Pool Count | Pool Size | Concurrency | Req/sec | Error % |
39 | Buoy | 1 | 1024 | 4096 | 101280 | 79.6 |
40 | Buoy | 1 | 1024 | 8192 | 97029 | 79.7 |
41 | Buoy | 1 | 1024 | 2048 | 96272 | 79.5 |
42 | Buoy | 1 | 1024 | 16384 | 85672 | 79.6 |
43 | Buoy | 1 | 512 | 4096 | 84345 | 89.8 |
44 | Buoy | 1 | 512 | 2048 | 78757 | 89.8 |
45 | Buoy | 1 | 512 | 8192 | 77945 | 90 |
46 | MachineGun | 1 | 1024 | 2048 | 72950 | 0 |
47 | Buoy | 1 | 512 | 16384 | 72247 | 90 |
48 | MachineGun | 1 | 1024 | 4096 | 72108 | 0 |
49 | MachineGun | 1 | 1024 | 1024 | 71219 | 0 |
50 | Buoy | 1 | 256 | 4096 | 62332 | 94.9 |
51 | Mojito | 4 | 256 | 4096 | 62068 | 0.8 |
52 | MachineGun | 1 | 1024 | 8192 | 61205 | 1 |
53 | Mojito | 8 | 128 | 4096 | 59354 | 0.8 |
54 | Mojito | 4 | 256 | 8192 | 59353 | 0.8 |
55 | Buoy | 1 | 1024 | 1024 | 59273 | 79.4 |
56 | Buoy | 1 | 256 | 2048 | 59069 | 94.9 |
57 | Mojito | 8 | 128 | 8192 | 58707 | 0.8 |
58 | Mojito | 8 | 256 | 8192 | 58066 | 0.8 |
59 | Mojito | 8 | 128 | 16384 | 57690 | 0.8 |
60 | Mojito | 16 | 64 | 2048 | 57468 | 0.8 |
61 | Mojito | 16 | 64 | 16384 | 57098 | 0.8 |
62 | Mojito | 16 | 128 | 8192 | 56712 | 0.8 |
63 | Mojito | 4 | 256 | 16384 | 56690 | 0.8 |
64 | Mojito | 16 | 64 | 8192 | 56645 | 0.8 |
65 | Mojito | 8 | 256 | 4096 | 56325 | 0.9 |
66 | Mojito | 8 | 256 | 16384 | 56198 | 0.8 |
67 | Buoy | 1 | 256 | 8192 | 55993 | 95.1 |
68 | Mojito | 16 | 512 | 4096 | 55633 | 0.6 |
69 | Mojito | 16 | 1024 | 2048 | 55272 | 0.7 |
70 | Mojito | 4 | 512 | 4096 | 55131 | 0.9 |
71 | MachineGun | 1 | 1024 | 16384 | 55055 | 2.9 |
72 | Mojito | 32 | 32 | 1024 | 54993 | 0.8 |
73 | Mojito | 32 | 32 | 8192 | 54943 | 0.8 |
74 | Mojito | 32 | 32 | 16384 | 54500 | 0.8 |
75 | Mojito | 16 | 1024 | 4096 | 54381 | 0.8 |
76 | Mojito | 32 | 64 | 8192 | 54240 | 0.8 |
77 | Mojito | 16 | 128 | 16384 | 53882 | 0.9 |
78 | Mojito | 1 | 1024 | 1024 | 53848 | 1.2 |
79 | Mojito | 4 | 512 | 2048 | 53814 | 0.8 |
80 | Mojito | 16 | 64 | 4096 | 53731 | 1 |
81 | Mojito | 16 | 128 | 4096 | 53702 | 0.9 |
82 | Mojito | 32 | 512 | 4096 | 53622 | 0.6 |
83 | Mojito | 16 | 128 | 2048 | 53610 | 0.9 |
84 | Mojito | 8 | 512 | 4096 | 53488 | 0.7 |
85 | Mojito | 32 | 32 | 2048 | 53451 | 0.8 |
86 | Mojito | 8 | 1024 | 2048 | 53283 | 0.9 |
87 | Mojito | 1 | 1024 | 2048 | 53091 | 1.5 |
88 | Mojito | 32 | 128 | 2048 | 53006 | 0.7 |
89 | Mojito | 16 | 256 | 4096 | 52844 | 0.9 |
90 | Mojito | 4 | 1024 | 4096 | 52789 | 0.8 |
91 | Mojito | 16 | 256 | 2048 | 52650 | 0.8 |
92 | Mojito | 32 | 64 | 16384 | 52511 | 0.9 |
93 | Mojito | 1 | 1024 | 4096 | 52297 | 0.9 |
94 | Mojito | 32 | 64 | 2048 | 52247 | 0.8 |
95 | Mojito | 4 | 1024 | 1024 | 52235 | 1.2 |
96 | Mojito | 32 | 64 | 4096 | 52169 | 0.9 |
97 | Mojito | 32 | 128 | 4096 | 52016 | 0.9 |
98 | Buoy | 1 | 256 | 16384 | 51785 | 95.1 |
99 | Mojito | 8 | 1024 | 4096 | 51780 | 0.7 |
100 | Mojito | 8 | 256 | 2048 | 51741 | 0.9 |
101 | Mojito | 4 | 1024 | 2048 | 51627 | 0.8 |
102 | Mojito | 4 | 512 | 8192 | 51388 | 0.9 |
103 | Mojito | 32 | 512 | 2048 | 51387 | 0.8 |
104 | Mojito | 8 | 512 | 2048 | 51378 | 0.9 |
105 | Mojito | 8 | 512 | 8192 | 51008 | 0.8 |
106 | Mojito | 32 | 256 | 2048 | 50834 | 0.8 |
107 | Mojito | 16 | 512 | 2048 | 50630 | 0.8 |
108 | Mojito | 8 | 1024 | 1024 | 50204 | 1.3 |
109 | Mojito | 4 | 256 | 2048 | 50108 | 1.3 |
110 | Mojito | 32 | 512 | 1024 | 50034 | 1.1 |
111 | Mojito | 32 | 64 | 1024 | 49968 | 0.9 |
112 | Mojito | 32 | 256 | 16384 | 49966 | 1.8 |
113 | Buoy | 1 | 512 | 1024 | 49709 | 89.8 |
114 | Mojito | 4 | 256 | 1024 | 49635 | 1.5 |
115 | Mojito | 16 | 256 | 1024 | 49613 | 1 |
116 | Mojito | 16 | 256 | 16384 | 49330 | 1.1 |
117 | Mojito | 8 | 512 | 1024 | 49264 | 1 |
118 | Mojito | 32 | 32 | 4096 | 49209 | 0.9 |
119 | Mojito | 32 | 256 | 4096 | 49164 | 0.8 |
120 | Mojito | 8 | 128 | 2048 | 49152 | 1.2 |
121 | Mojito | 32 | 128 | 8192 | 48732 | 0.9 |
122 | Mojito | 16 | 256 | 8192 | 48395 | 0.9 |
123 | Mojito | 8 | 128 | 1024 | 48349 | 1.5 |
124 | Mojito | 4 | 512 | 1024 | 48100 | 1.2 |
125 | Mojito | 32 | 128 | 16384 | 48067 | 1.1 |
126 | Mojito | 16 | 128 | 1024 | 47859 | 1.3 |
127 | Mojito | 16 | 512 | 1024 | 47576 | 1.3 |
128 | Mojito | 1 | 1024 | 8192 | 47573 | 0.8 |
129 | Mojito | 8 | 256 | 1024 | 47218 | 1.2 |
130 | Mojito | 32 | 256 | 8192 | 46845 | 1.7 |
131 | Mojito | 16 | 512 | 8192 | 46811 | 1.4 |
132 | Mojito | 16 | 1024 | 1024 | 46721 | 1.3 |
133 | Mojito | 4 | 1024 | 8192 | 46195 | 1.3 |
134 | Mojito | 16 | 512 | 16384 | 46190 | 1.6 |
135 | Mojito | 32 | 128 | 1024 | 45386 | 1.2 |
136 | Mojito | 32 | 256 | 1024 | 45115 | 1.2 |
137 | Mojito | 16 | 64 | 1024 | 45082 | 1.4 |
138 | Mojito | 4 | 512 | 16384 | 44398 | 1.2 |
139 | Mojito | 8 | 1024 | 16384 | 43890 | 2.1 |
140 | MachineGun | 1 | 512 | 1024 | 43529 | 0 |
141 | MachineGun | 1 | 512 | 2048 | 43491 | 0 |
142 | Mojito | 4 | 128 | 4096 | 43476 | 0.9 |
143 | Mojito | 8 | 64 | 8192 | 43327 | 0.9 |
144 | Mojito | 8 | 64 | 16384 | 43300 | 0.9 |
145 | Mojito | 4 | 128 | 8192 | 43240 | 0.9 |
146 | Mojito | 4 | 128 | 2048 | 43067 | 0.9 |
147 | Mojito | 16 | 32 | 4096 | 42978 | 0.9 |
148 | Mojito | 16 | 32 | 8192 | 42936 | 0.9 |
149 | Mojito | 4 | 128 | 16384 | 42892 | 0.9 |
150 | Mojito | 16 | 32 | 16384 | 42858 | 0.9 |
151 | Mojito | 32 | 512 | 8192 | 42817 | 1.4 |
152 | Mojito | 8 | 64 | 2048 | 42718 | 0.9 |
153 | Mojito | 8 | 64 | 4096 | 42662 | 0.9 |
154 | Mojito | 4 | 128 | 1024 | 42556 | 0.9 |
155 | Mojito | 1 | 512 | 2048 | 42497 | 0.9 |
156 | Mojito | 16 | 32 | 2048 | 42367 | 0.9 |
157 | MachineGun | 1 | 512 | 4096 | 42362 | 0 |
158 | Mojito | 1 | 512 | 4096 | 42187 | 0.9 |
159 | Mojito | 8 | 64 | 1024 | 41979 | 0.9 |
160 | Mojito | 16 | 32 | 1024 | 41876 | 0.9 |
161 | Buoy | 1 | 128 | 4096 | 40492 | 97.5 |
162 | Buoy | 1 | 256 | 1024 | 40148 | 94.9 |
163 | MachineGun | 1 | 512 | 8192 | 39488 | 1.1 |
164 | Mojito | 1 | 512 | 1024 | 39362 | 1 |
165 | Mojito | 1 | 512 | 8192 | 37746 | 0.9 |
166 | MachineGun | 1 | 512 | 16384 | 37613 | 3.1 |
167 | Buoy | 1 | 128 | 2048 | 37507 | 97.5 |
168 | Mojito | 32 | 1024 | 8192 | 36927 | 0.7 |
169 | Buoy | 1 | 128 | 8192 | 36476 | 97.5 |
170 | Mojito | 16 | 1024 | 8192 | 33914 | 0.8 |
171 | Ibrowse | 1 | 32 | 8192 | 33847 | 4.4 |
172 | Mojito | 32 | 512 | 16384 | 33554 | 2.2 |
173 | Ibrowse | 1 | 32 | 4096 | 33002 | 3.9 |
174 | Ibrowse | 1 | 32 | 2048 | 31958 | 2.9 |
175 | Buoy | 1 | 128 | 16384 | 31956 | 97.6 |
176 | Mojito | 32 | 1024 | 4096 | 31289 | 3.1 |
177 | Buoy | 1 | 128 | 1024 | 30956 | 97.4 |
178 | Mojito | 8 | 1024 | 8192 | 30955 | 2.4 |
179 | Mojito | 1 | 512 | 16384 | 30296 | 5.2 |
180 | Ibrowse | 1 | 32 | 16384 | 30091 | 4.9 |
181 | Mojito | 1 | 1024 | 16384 | 28546 | 8.6 |
182 | Dlhttpc | 1 | 1024 | 1024 | 28443 | 70.5 |
183 | Mojito | 16 | 1024 | 16384 | 27933 | 2.4 |
184 | Mojito | 8 | 512 | 16384 | 27761 | 2.2 |
185 | Ibrowse | 1 | 32 | 1024 | 27589 | 0.1 |
186 | Ibrowse | 1 | 64 | 4096 | 26942 | 0.2 |
187 | Ibrowse | 1 | 64 | 2048 | 26386 | 0.4 |
188 | Ibrowse | 1 | 64 | 8192 | 26133 | 0.7 |
189 | Mojito | 32 | 1024 | 2048 | 25969 | 4.4 |
190 | Mojito | 32 | 1024 | 16384 | 24921 | 2.8 |
191 | Ibrowse | 1 | 64 | 16384 | 24618 | 0.4 |
192 | Ibrowse | 1 | 128 | 4096 | 23537 | 0 |
193 | Dlhttpc | 1 | 1024 | 8192 | 23528 | 95.2 |
194 | Ibrowse | 1 | 64 | 1024 | 23308 | 0 |
195 | MachineGun | 1 | 256 | 1024 | 23134 | 0.1 |
196 | MachineGun | 1 | 256 | 2048 | 23058 | 0.1 |
197 | Mojito | 4 | 1024 | 16384 | 22908 | 4.3 |
198 | MachineGun | 1 | 256 | 4096 | 22816 | 0.1 |
199 | Buoy | 1 | 64 | 4096 | 22782 | 98.7 |
200 | Mojito | 4 | 64 | 4096 | 22759 | 1 |
201 | Mojito | 1 | 256 | 1024 | 22702 | 1 |
202 | Mojito | 4 | 64 | 16384 | 22666 | 1 |
203 | Mojito | 1 | 256 | 2048 | 22637 | 1 |
204 | Mojito | 8 | 32 | 8192 | 22620 | 1 |
205 | Mojito | 1 | 256 | 4096 | 22604 | 1 |
206 | Mojito | 4 | 64 | 8192 | 22598 | 1 |
207 | Mojito | 8 | 32 | 16384 | 22469 | 1 |
208 | Mojito | 8 | 32 | 4096 | 22469 | 1 |
209 | Ibrowse | 1 | 128 | 2048 | 22469 | 0.1 |
210 | Mojito | 4 | 64 | 2048 | 22463 | 1 |
211 | Mojito | 8 | 32 | 2048 | 22384 | 1 |
212 | Ibrowse | 1 | 128 | 8192 | 22330 | 0 |
213 | Hackney | 1 | 256 | 1024 | 22316 | 0 |
214 | Mojito | 4 | 64 | 1024 | 22296 | 1 |
215 | Mojito | 8 | 32 | 1024 | 22239 | 1 |
216 | MachineGun | 1 | 256 | 8192 | 22063 | 1 |
217 | Buoy | 1 | 64 | 2048 | 21941 | 98.7 |
218 | Hackney | 1 | 256 | 2048 | 21694 | 0 |
219 | MachineGun | 1 | 256 | 16384 | 21479 | 3.3 |
220 | Ibrowse | 1 | 128 | 16384 | 21437 | 0.1 |
221 | Mojito | 1 | 256 | 8192 | 21220 | 1.1 |
222 | Buoy | 1 | 64 | 8192 | 21124 | 98.7 |
223 | Hackney | 1 | 256 | 4096 | 21031 | 0 |
224 | Ibrowse | 1 | 128 | 1024 | 20333 | 0 |
225 | Buoy | 1 | 64 | 16384 | 20244 | 98.8 |
226 | Dlhttpc | 1 | 512 | 2048 | 19964 | 94.9 |
227 | Buoy | 1 | 64 | 1024 | 19597 | 98.7 |
228 | Hackney | 1 | 256 | 8192 | 19085 | 0 |
229 | Mojito | 1 | 256 | 16384 | 19067 | 5.8 |
230 | Hackney | 1 | 512 | 1024 | 18643 | 0 |
231 | Ibrowse | 1 | 256 | 4096 | 18626 | 0 |
232 | Dlhttpc | 1 | 1024 | 16384 | 18513 | 96.5 |
233 | Hackney | 1 | 512 | 4096 | 18133 | 0 |
234 | Ibrowse | 1 | 256 | 2048 | 17955 | 0 |
235 | Ibrowse | 1 | 256 | 8192 | 17925 | 0 |
236 | Hackney | 1 | 256 | 16384 | 17633 | 0 |
237 | Dlhttpc | 1 | 1024 | 2048 | 17571 | 87.6 |
238 | Hackney | 1 | 512 | 2048 | 17538 | 0 |
239 | Ibrowse | 1 | 256 | 16384 | 17126 | 0 |
240 | Dlhttpc | 1 | 1024 | 4096 | 16957 | 92.6 |
241 | Ibrowse | 1 | 256 | 1024 | 16948 | 0 |
242 | Mojito | 4 | 32 | 16384 | 15805 | 6.2 |
243 | Mojito | 32 | 1024 | 1024 | 15511 | 8.5 |
244 | Hackney | 1 | 512 | 8192 | 15265 | 0 |
245 | Dlhttpc | 1 | 512 | 8192 | 13699 | 97.6 |
246 | Ibrowse | 1 | 512 | 4096 | 13698 | 0 |
247 | Mojito | 1 | 128 | 16384 | 13285 | 14.7 |
248 | Ibrowse | 1 | 512 | 8192 | 13145 | 0 |
249 | Ibrowse | 1 | 512 | 2048 | 13078 | 0 |
250 | Ibrowse | 1 | 512 | 16384 | 12705 | 0 |
251 | Hackney | 1 | 512 | 16384 | 12579 | 0 |
252 | Buoy | 1 | 32 | 2048 | 12492 | 99.4 |
253 | Ibrowse | 1 | 512 | 1024 | 12369 | 0 |
254 | Buoy | 1 | 32 | 8192 | 11657 | 99.4 |
255 | MachineGun | 1 | 128 | 1024 | 11615 | 0.1 |
256 | MachineGun | 1 | 128 | 2048 | 11606 | 0.1 |
257 | Mojito | 1 | 128 | 1024 | 11603 | 1 |
258 | Mojito | 1 | 128 | 2048 | 11596 | 1 |
259 | Hackney | 1 | 1024 | 2048 | 11573 | 0 |
260 | Mojito | 4 | 32 | 4096 | 11571 | 1 |
261 | MachineGun | 1 | 128 | 4096 | 11570 | 0.1 |
262 | Mojito | 1 | 128 | 4096 | 11568 | 1 |
263 | Hackney | 1 | 1024 | 1024 | 11566 | 0 |
264 | Mojito | 4 | 32 | 2048 | 11556 | 1 |
265 | Mojito | 4 | 32 | 8192 | 11550 | 1 |
266 | Mojito | 4 | 32 | 1024 | 11517 | 1 |
267 | Hackney | 1 | 128 | 1024 | 11465 | 0 |
268 | Buoy | 1 | 32 | 4096 | 11463 | 99.4 |
269 | Buoy | 1 | 32 | 16384 | 11443 | 99.4 |
270 | Hackney | 1 | 128 | 2048 | 11438 | 0 |
271 | MachineGun | 1 | 128 | 8192 | 11432 | 0.9 |
272 | Hackney | 1 | 128 | 4096 | 11430 | 0 |
273 | Mojito | 1 | 128 | 8192 | 11249 | 1.3 |
274 | Hackney | 1 | 128 | 8192 | 11152 | 0 |
275 | Buoy | 1 | 32 | 1024 | 10956 | 99.4 |
276 | Hackney | 1 | 1024 | 4096 | 10725 | 0 |
277 | Hackney | 1 | 128 | 16384 | 10544 | 0 |
278 | Dlhttpc | 1 | 256 | 2048 | 10270 | 97.7 |
279 | Hackney | 1 | 1024 | 8192 | 9662 | 0 |
280 | Dlhttpc | 1 | 256 | 1024 | 9114 | 96.8 |
281 | Hackney | 1 | 1024 | 16384 | 8551 | 0 |
282 | Dlhttpc | 1 | 512 | 1024 | 8393 | 89.9 |
283 | Ibrowse | 1 | 1024 | 4096 | 8365 | 0 |
284 | Dlhttpc | 1 | 512 | 4096 | 8319 | 95.9 |
285 | Ibrowse | 1 | 1024 | 2048 | 8149 | 0 |
286 | Ibrowse | 1 | 1024 | 8192 | 8121 | 0 |
287 | Mojito | 1 | 64 | 8192 | 7993 | 15.3 |
288 | Dlhttpc | 1 | 256 | 8192 | 7966 | 98.7 |
289 | Ibrowse | 1 | 1024 | 16384 | 7962 | 0 |
290 | Ibrowse | 1 | 1024 | 1024 | 7828 | 0 |
291 | Dlhttpc | 1 | 512 | 16384 | 6968 | 98.2 |
292 | Dlhttpc | 1 | 256 | 16384 | 6722 | 99 |
293 | MachineGun | 1 | 64 | 1024 | 5810 | 0.1 |
294 | MachineGun | 1 | 64 | 4096 | 5807 | 0.1 |
295 | MachineGun | 1 | 64 | 2048 | 5807 | 0.1 |
296 | Mojito | 1 | 64 | 2048 | 5806 | 1 |
297 | Mojito | 1 | 64 | 1024 | 5806 | 1 |
298 | Mojito | 1 | 64 | 4096 | 5805 | 1 |
299 | Hackney | 1 | 64 | 1024 | 5791 | 0 |
300 | Hackney | 1 | 64 | 2048 | 5787 | 0 |
301 | Hackney | 1 | 64 | 4096 | 5781 | 0 |
302 | Dlhttpc | 1 | 128 | 1024 | 5753 | 98.6 |
303 | Hackney | 1 | 64 | 8192 | 5746 | 0 |
304 | Httpc | 1 | 32 | 1024 | 5742 | 0 |
305 | Hackney | 1 | 64 | 16384 | 5682 | 0 |
306 | Dlhttpc | 1 | 128 | 2048 | 5680 | 99 |
307 | Httpc | 1 | 1024 | 2048 | 5571 | 0 |
308 | Httpc | 1 | 256 | 1024 | 5568 | 0 |
309 | Mojito | 1 | 32 | 4096 | 5562 | 17.4 |
310 | Httpc | 1 | 64 | 1024 | 5560 | 0 |
311 | Httpc | 1 | 128 | 1024 | 5543 | 0 |
312 | Httpc | 1 | 256 | 4096 | 5532 | 0 |
313 | Httpc | 1 | 512 | 2048 | 5488 | 0 |
314 | Httpc | 1 | 512 | 1024 | 5467 | 0 |
315 | Httpc | 1 | 128 | 4096 | 5408 | 0 |
316 | Httpc | 1 | 1024 | 4096 | 5397 | 0 |
317 | Httpc | 1 | 1024 | 1024 | 5340 | 0 |
318 | Httpc | 1 | 256 | 2048 | 5263 | 0 |
319 | Httpc | 1 | 32 | 2048 | 5231 | 0 |
320 | Httpc | 1 | 64 | 2048 | 5186 | 0 |
321 | Dlhttpc | 1 | 128 | 4096 | 5184 | 99.2 |
322 | Httpc | 1 | 128 | 2048 | 5143 | 0 |
323 | Httpc | 1 | 32 | 8192 | 5082 | 0 |
324 | Httpc | 1 | 32 | 4096 | 5031 | 0 |
325 | Httpc | 1 | 256 | 8192 | 5013 | 0 |
326 | Httpc | 1 | 64 | 8192 | 5001 | 0 |
327 | Httpc | 1 | 512 | 4096 | 4998 | 0 |
328 | Mojito | 1 | 32 | 8192 | 4991 | 31.8 |
329 | Httpc | 1 | 512 | 8192 | 4973 | 0 |
330 | Httpc | 1 | 64 | 4096 | 4971 | 0 |
331 | Httpc | 1 | 1024 | 8192 | 4718 | 0 |
332 | Httpc | 1 | 64 | 16384 | 4590 | 0 |
333 | Httpc | 1 | 128 | 8192 | 4540 | 0 |
334 | Httpc | 1 | 256 | 16384 | 4525 | 0 |
335 | Httpc | 1 | 32 | 16384 | 4427 | 0 |
336 | Httpc | 1 | 1024 | 16384 | 4408 | 0 |
337 | Dlhttpc | 1 | 128 | 8192 | 4364 | 99.4 |
338 | Httpc | 1 | 128 | 16384 | 4298 | 0 |
339 | Httpc | 1 | 512 | 16384 | 4268 | 0 |
340 | Dlhttpc | 1 | 128 | 16384 | 3693 | 99.5 |
341 | Mint | 1 | 1 | 2048 | 3474 | 2 |
342 | Mint | 1 | 1 | 1024 | 3440 | 2 |
343 | Dlhttpc | 1 | 256 | 4096 | 3374 | 98.4 |
344 | Mint | 1 | 1 | 8192 | 3326 | 6.6 |
345 | Dlhttpc | 1 | 64 | 1024 | 3178 | 99.4 |
346 | Dlhttpc | 1 | 64 | 2048 | 3170 | 99.5 |
347 | Mint | 1 | 1 | 4096 | 3114 | 8.7 |
348 | MachineGun | 1 | 32 | 2048 | 2906 | 0.1 |
349 | Mojito | 1 | 32 | 2048 | 2903 | 1 |
350 | Mojito | 1 | 32 | 1024 | 2903 | 1 |
351 | Hackney | 1 | 32 | 1024 | 2898 | 0 |
352 | Hackney | 1 | 32 | 4096 | 2897 | 0 |
353 | Hackney | 1 | 32 | 2048 | 2897 | 0 |
354 | Hackney | 1 | 32 | 8192 | 2896 | 0 |
355 | MachineGun | 1 | 32 | 1024 | 2891 | 0 |
356 | Hackney | 1 | 32 | 16384 | 2885 | 0 |
357 | Dlhttpc | 1 | 64 | 4096 | 2622 | 99.6 |
358 | MachineGun | 1 | 32 | 4096 | 2362 | 42.2 |
359 | Dlhttpc | 1 | 64 | 8192 | 2155 | 99.7 |
360 | Dlhttpc | 1 | 64 | 16384 | 2091 | 99.7 |
361 | MachineGun | 1 | 64 | 8192 | 1871 | 77.1 |
362 | Dlhttpc | 1 | 32 | 2048 | 1681 | 99.8 |
363 | Mint | 1 | 1 | 16384 | 1658 | 0 |
364 | Dlhttpc | 1 | 32 | 1024 | 1606 | 99.7 |
365 | Dlhttpc | 1 | 32 | 4096 | 1411 | 99.8 |
366 | Dlhttpc | 1 | 32 | 8192 | 1209 | 99.9 |
367 | Dlhttpc | 1 | 32 | 16384 | 1022 | 99.9 |
368 | MachineGun | 1 | 128 | 16384 | 467 | 97.1 |
369 | MachineGun | 1 | 64 | 16384 | 196 | 98.8 |
370 | Mojito | 1 | 64 | 16384 | 96 | 98.8 |
371 | MachineGun | 1 | 32 | 16384 | 96 | 99.4 |
372 | MachineGun | 1 | 32 | 8192 | 48 | 99.4 |
373 | Mojito | 1 | 32 | 16384 | 47 | 99.4 |
374 |
375 |
376 |
377 |
378 |
379 |
380 | ## The original benchmark
381 |
382 | With server specs as above, except using Ubuntu's default `ulimit -n 1024`.
383 | This benchmark rightly showed that Mojito is fast, but wrongly showed it
384 | being faster than Buoy. It does tell us that Mojito excels in
385 | resource-constrained environments, or where configuring custom `ulimit`s
386 | is not possible.
387 |
388 |
389 | Client | Pool Count | Pool Size | Concurrency | Req/sec | Error % |
390 | Client | Pool Count | Pool Size | Concurrency | Req/sec | Error % |
391 | Mojito | 4 | 256 | 2048 | 52519 | 1.2 |
392 | Mojito | 8 | 128 | 4096 | 52105 | 1.1 |
393 | Mojito | 8 | 128 | 2048 | 51621 | 1 |
394 | Mojito | 8 | 128 | 16384 | 51538 | 0.9 |
395 | Mojito | 8 | 128 | 8192 | 51287 | 1 |
396 | Mojito | 4 | 256 | 4096 | 51116 | 1.2 |
397 | Mojito | 16 | 64 | 2048 | 51064 | 1.1 |
398 | Mojito | 32 | 32 | 4096 | 50930 | 0.9 |
399 | Mojito | 16 | 64 | 8192 | 50848 | 1 |
400 | Mojito | 16 | 64 | 4096 | 50336 | 0.9 |
401 | Mojito | 16 | 64 | 16384 | 50294 | 1 |
402 | Mojito | 4 | 256 | 8192 | 49134 | 1.1 |
403 | Mojito | 32 | 32 | 16384 | 48470 | 1 |
404 | Mojito | 32 | 32 | 8192 | 48103 | 0.9 |
405 | Mojito | 32 | 32 | 2048 | 47914 | 1 |
406 | Mojito | 4 | 256 | 16384 | 46866 | 1.2 |
407 | Mojito | 8 | 64 | 8192 | 43531 | 0.9 |
408 | Mojito | 1 | 512 | 2048 | 43437 | 0.9 |
409 | Mojito | 4 | 128 | 8192 | 43224 | 0.9 |
410 | Mojito | 4 | 128 | 2048 | 43183 | 0.9 |
411 | Mojito | 4 | 128 | 16384 | 43128 | 0.9 |
412 | Mojito | 4 | 128 | 4096 | 42845 | 0.9 |
413 | Mojito | 8 | 64 | 16384 | 42537 | 0.9 |
414 | Mojito | 1 | 512 | 4096 | 42255 | 0.9 |
415 | Mojito | 8 | 64 | 2048 | 42020 | 0.9 |
416 | Mojito | 8 | 64 | 4096 | 41900 | 0.9 |
417 | Mojito | 16 | 32 | 4096 | 41842 | 0.9 |
418 | Mojito | 16 | 32 | 16384 | 41480 | 0.9 |
419 | Mojito | 16 | 32 | 8192 | 41318 | 0.9 |
420 | Mojito | 16 | 32 | 2048 | 40702 | 0.9 |
421 | Mojito | 1 | 512 | 8192 | 37407 | 1 |
422 | Buoy | 1 | 512 | 8192 | 36833 | 89.8 |
423 | Buoy | 1 | 512 | 16384 | 34942 | 89.6 |
424 | Buoy | 1 | 512 | 4096 | 34417 | 89.8 |
425 | Buoy | 1 | 512 | 2048 | 31889 | 89.8 |
426 | Mojito | 1 | 512 | 16384 | 30870 | 5.6 |
427 | Mojito | 4 | 512 | 2048 | 26924 | 2 |
428 | Mojito | 16 | 128 | 2048 | 24494 | 1.7 |
429 | Mojito | 32 | 64 | 2048 | 24210 | 1.4 |
430 | Mojito | 4 | 64 | 16384 | 22923 | 1 |
431 | Mojito | 1 | 256 | 2048 | 22843 | 1 |
432 | Mojito | 8 | 32 | 8192 | 22745 | 1 |
433 | Mojito | 4 | 64 | 2048 | 22727 | 1 |
434 | Mojito | 4 | 64 | 4096 | 22698 | 1 |
435 | Mojito | 4 | 64 | 8192 | 22677 | 1 |
436 | Mojito | 8 | 32 | 4096 | 22677 | 1 |
437 | Mojito | 8 | 32 | 16384 | 22644 | 1 |
438 | Mojito | 1 | 256 | 4096 | 22588 | 1 |
439 | Mojito | 8 | 32 | 2048 | 22282 | 1 |
440 | Hackney | 1 | 256 | 2048 | 21885 | 0 |
441 | Mojito | 1 | 256 | 8192 | 21336 | 1.1 |
442 | Hackney | 1 | 256 | 4096 | 21226 | 0 |
443 | Mojito | 32 | 64 | 16384 | 21054 | 8.8 |
444 | Mojito | 16 | 128 | 16384 | 19618 | 11.6 |
445 | Dlhttpc | 1 | 512 | 2048 | 19493 | 95.1 |
446 | Mojito | 1 | 256 | 16384 | 19310 | 5.4 |
447 | Hackney | 1 | 256 | 8192 | 19033 | 0 |
448 | Buoy | 1 | 256 | 4096 | 18692 | 94.9 |
449 | Buoy | 1 | 256 | 8192 | 18531 | 94.9 |
450 | Mojito | 8 | 256 | 16384 | 18409 | 7.2 |
451 | Buoy | 1 | 256 | 16384 | 18052 | 94.8 |
452 | Ibrowse | 1 | 256 | 2048 | 17655 | 0 |
453 | Ibrowse | 1 | 256 | 4096 | 17626 | 0 |
454 | Hackney | 1 | 512 | 2048 | 17619 | 0 |
455 | Hackney | 1 | 256 | 16384 | 17272 | 0 |
456 | Mojito | 4 | 512 | 8192 | 17157 | 9.5 |
457 | Mojito | 8 | 256 | 4096 | 17022 | 4.8 |
458 | Ibrowse | 1 | 256 | 8192 | 17007 | 0 |
459 | Buoy | 1 | 256 | 2048 | 16973 | 94.9 |
460 | Hackney | 1 | 512 | 4096 | 16821 | 0 |
461 | Ibrowse | 1 | 256 | 16384 | 16527 | 0 |
462 | Mojito | 16 | 128 | 4096 | 15636 | 3.9 |
463 | Mojito | 8 | 256 | 2048 | 15459 | 1.6 |
464 | Mojito | 8 | 512 | 2048 | 15385 | 2 |
465 | Mojito | 16 | 512 | 2048 | 15334 | 1.8 |
466 | Mojito | 8 | 256 | 8192 | 15332 | 9.7 |
467 | Mojito | 4 | 512 | 4096 | 15318 | 5.1 |
468 | Mojito | 32 | 64 | 4096 | 15179 | 6.2 |
469 | Hackney | 1 | 512 | 8192 | 14954 | 0 |
470 | Dlhttpc | 1 | 512 | 8192 | 14884 | 97.3 |
471 | Mojito | 32 | 64 | 8192 | 14882 | 8.5 |
472 | Mojito | 16 | 256 | 2048 | 14794 | 1.9 |
473 | Mojito | 32 | 512 | 2048 | 14780 | 1.9 |
474 | Mojito | 16 | 128 | 8192 | 14673 | 10.6 |
475 | Mojito | 4 | 512 | 16384 | 14638 | 14.8 |
476 | Ibrowse | 1 | 512 | 2048 | 13318 | 0 |
477 | Ibrowse | 1 | 512 | 4096 | 13291 | 0 |
478 | Hackney | 1 | 512 | 16384 | 13061 | 0 |
479 | Ibrowse | 1 | 512 | 8192 | 12787 | 0 |
480 | Mojito | 32 | 128 | 2048 | 12315 | 2.6 |
481 | Ibrowse | 1 | 512 | 16384 | 12271 | 0 |
482 | Mojito | 32 | 256 | 2048 | 12050 | 2.6 |
483 | Mojito | 32 | 128 | 4096 | 11634 | 4.6 |
484 | Mojito | 1 | 128 | 2048 | 11602 | 1 |
485 | Mojito | 4 | 32 | 2048 | 11583 | 1 |
486 | Ibrowse | 1 | 128 | 2048 | 11567 | 0.1 |
487 | Mojito | 1 | 128 | 4096 | 11552 | 1 |
488 | Mojito | 4 | 32 | 4096 | 11540 | 1 |
489 | Dlhttpc | 1 | 512 | 16384 | 11493 | 98.1 |
490 | Hackney | 1 | 128 | 2048 | 11491 | 0 |
491 | Ibrowse | 1 | 128 | 4096 | 11488 | 0 |
492 | Mojito | 32 | 128 | 16384 | 11462 | 19.3 |
493 | Hackney | 1 | 128 | 4096 | 11418 | 0 |
494 | Mojito | 32 | 128 | 8192 | 11361 | 10.8 |
495 | Mojito | 16 | 256 | 8192 | 11276 | 11.8 |
496 | Mojito | 1 | 128 | 8192 | 11260 | 1.3 |
497 | Ibrowse | 1 | 128 | 8192 | 11232 | 0 |
498 | Hackney | 1 | 128 | 8192 | 11151 | 0 |
499 | Mojito | 4 | 32 | 16384 | 11015 | 48.2 |
500 | Mojito | 4 | 32 | 8192 | 10855 | 1.7 |
501 | Dlhttpc | 1 | 256 | 2048 | 10737 | 97.7 |
502 | Ibrowse | 1 | 128 | 16384 | 10655 | 3.8 |
503 | Hackney | 1 | 128 | 16384 | 10590 | 0 |
504 | Dlhttpc | 1 | 256 | 4096 | 10181 | 98.3 |
505 | Mojito | 16 | 256 | 4096 | 10162 | 4.3 |
506 | Mojito | 8 | 512 | 16384 | 10153 | 14.6 |
507 | Mojito | 32 | 256 | 4096 | 9852 | 4.9 |
508 | Mojito | 16 | 512 | 4096 | 9673 | 5.7 |
509 | Buoy | 1 | 128 | 4096 | 9626 | 97.4 |
510 | Mojito | 8 | 512 | 4096 | 9617 | 7.9 |
511 | Mojito | 8 | 512 | 8192 | 9518 | 11.3 |
512 | Mojito | 1 | 128 | 16384 | 9425 | 47.4 |
513 | Buoy | 1 | 128 | 8192 | 9403 | 97.4 |
514 | Buoy | 1 | 128 | 2048 | 9036 | 97.4 |
515 | Buoy | 1 | 128 | 16384 | 8624 | 97.6 |
516 | Mojito | 16 | 256 | 16384 | 8524 | 31.6 |
517 | Dlhttpc | 1 | 256 | 8192 | 8110 | 98.7 |
518 | Dlhttpc | 1 | 512 | 4096 | 7998 | 96.3 |
519 | Mojito | 32 | 512 | 4096 | 7538 | 16.2 |
520 | Dlhttpc | 1 | 256 | 16384 | 7152 | 98.8 |
521 | Mojito | 16 | 512 | 8192 | 6690 | 16.5 |
522 | Mojito | 32 | 256 | 8192 | 6547 | 12.7 |
523 | Mojito | 16 | 512 | 16384 | 6467 | 17.6 |
524 | Mojito | 32 | 256 | 16384 | 6234 | 21.8 |
525 | Mojito | 1 | 64 | 2048 | 5810 | 1 |
526 | Mojito | 1 | 64 | 4096 | 5809 | 1 |
527 | Ibrowse | 1 | 64 | 4096 | 5805 | 0 |
528 | Hackney | 1 | 64 | 2048 | 5798 | 0 |
529 | Hackney | 1 | 64 | 4096 | 5791 | 0 |
530 | Ibrowse | 1 | 64 | 2048 | 5791 | 0 |
531 | Ibrowse | 1 | 64 | 8192 | 5788 | 3.4 |
532 | Hackney | 1 | 64 | 8192 | 5758 | 0 |
533 | Ibrowse | 1 | 64 | 16384 | 5756 | 30 |
534 | Mojito | 32 | 512 | 8192 | 5715 | 24.4 |
535 | Httpc | 1 | 64 | 2048 | 5698 | 0 |
536 | Httpc | 1 | 128 | 2048 | 5691 | 0 |
537 | Hackney | 1 | 64 | 16384 | 5673 | 0 |
538 | Httpc | 1 | 256 | 2048 | 5506 | 0 |
539 | Httpc | 1 | 32 | 4096 | 5464 | 0 |
540 | Httpc | 1 | 512 | 2048 | 5438 | 0 |
541 | Httpc | 1 | 512 | 4096 | 5436 | 0 |
542 | Httpc | 1 | 128 | 4096 | 5370 | 0 |
543 | Dlhttpc | 1 | 128 | 2048 | 5319 | 99 |
544 | Httpc | 1 | 64 | 4096 | 5300 | 0 |
545 | Httpc | 1 | 32 | 2048 | 5265 | 0 |
546 | Httpc | 1 | 128 | 8192 | 5173 | 0 |
547 | Dlhttpc | 1 | 128 | 4096 | 5170 | 99.2 |
548 | Httpc | 1 | 256 | 4096 | 5026 | 0 |
549 | Httpc | 1 | 64 | 8192 | 4885 | 0 |
550 | Httpc | 1 | 512 | 8192 | 4884 | 0 |
551 | Mojito | 1 | 64 | 8192 | 4878 | 49.6 |
552 | Httpc | 1 | 128 | 16384 | 4828 | 0 |
553 | Buoy | 1 | 64 | 2048 | 4793 | 98.7 |
554 | Buoy | 1 | 64 | 4096 | 4774 | 98.7 |
555 | Httpc | 1 | 32 | 16384 | 4564 | 0 |
556 | Mojito | 32 | 512 | 16384 | 4525 | 19 |
557 | Httpc | 1 | 512 | 16384 | 4514 | 0 |
558 | Httpc | 1 | 256 | 8192 | 4480 | 0 |
559 | Buoy | 1 | 64 | 8192 | 4427 | 98.8 |
560 | Httpc | 1 | 32 | 8192 | 4421 | 0 |
561 | Httpc | 1 | 256 | 16384 | 4367 | 0 |
562 | Buoy | 1 | 64 | 16384 | 4321 | 98.8 |
563 | Dlhttpc | 1 | 128 | 8192 | 4122 | 99.4 |
564 | Httpc | 1 | 64 | 16384 | 4087 | 0 |
565 | Dlhttpc | 1 | 128 | 16384 | 3934 | 99.4 |
566 | Mojito | 1 | 32 | 2048 | 2905 | 1 |
567 | Hackney | 1 | 32 | 2048 | 2903 | 0 |
568 | Hackney | 1 | 32 | 4096 | 2902 | 0 |
569 | Ibrowse | 1 | 32 | 16384 | 2901 | 55.1 |
570 | Ibrowse | 1 | 32 | 4096 | 2900 | 3.8 |
571 | Hackney | 1 | 32 | 8192 | 2899 | 0 |
572 | Ibrowse | 1 | 32 | 8192 | 2892 | 31.5 |
573 | Ibrowse | 1 | 32 | 2048 | 2887 | 0.1 |
574 | Hackney | 1 | 32 | 16384 | 2883 | 0 |
575 | Dlhttpc | 1 | 64 | 2048 | 2860 | 99.5 |
576 | Dlhttpc | 1 | 64 | 4096 | 2745 | 99.6 |
577 | Mojito | 1 | 32 | 4096 | 2477 | 59.5 |
578 | Buoy | 1 | 32 | 2048 | 2386 | 99.4 |
579 | Dlhttpc | 1 | 64 | 8192 | 2282 | 99.7 |
580 | Buoy | 1 | 32 | 4096 | 2203 | 99.4 |
581 | Buoy | 1 | 32 | 8192 | 2160 | 99.4 |
582 | Buoy | 1 | 32 | 16384 | 2144 | 99.4 |
583 | Dlhttpc | 1 | 64 | 16384 | 2072 | 99.7 |
584 | Mojito | 1 | 32 | 8192 | 1897 | 74.2 |
585 | Dlhttpc | 1 | 32 | 2048 | 1592 | 99.8 |
586 | Dlhttpc | 1 | 32 | 4096 | 1500 | 99.8 |
587 | Dlhttpc | 1 | 32 | 8192 | 1195 | 99.8 |
588 | Dlhttpc | 1 | 32 | 16384 | 1119 | 99.8 |
589 | Mint | 1 | 1 | 16384 | 254 | 79 |
590 | Mint | 1 | 1 | 8192 | 227 | 51.9 |
591 | Mint | 1 | 1 | 4096 | 206 | 35 |
592 | Mojito | 1 | 64 | 16384 | 97 | 98.8 |
593 | Mojito | 1 | 32 | 16384 | 47 | 99.4 |
594 |
595 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.7.10 (2021-10-27)
4 |
5 | Fixed a bug around starting pools. Thanks,
6 | [@reisub](https://github.com/reisub)!
7 |
8 | Marked compatibility with Telemetry 1.0. Thanks,
9 | [@jchristgit](https://github.com/jchristgit)!
10 |
11 | ## 0.7.9 (2021-07-20)
12 |
13 | Improved docs. Thanks, [@kianmeng](https://github.com/kianmeng)!
14 |
15 | ## 0.7.8 (2021-07-16)
16 |
17 | Fixed a few bugs around connection handling and chunk sizing. Thanks to
18 | [@reisub](https://github.com/reisub), [@fahchen](https://github.com/fahchen),
19 | [@bmteller](https://github.com/bmteller).
20 |
21 | ## 0.7.7 (2021-02-04)
22 |
23 | Added Mojito.Telemetry. Thanks,
24 | [@andyleclair](https://github.com/andyleclair)! And thanks to the
25 | [Finch](https://github.com/keathley/finch) team, whose telemetry
26 | implementation informed this one.
27 |
28 | ## 0.7.6 (2020-12-10)
29 |
30 | Fixed a bug around HTTP/2 responses larger than 64kB. Thanks for the
31 | reports, [@dch](https://github.com/dch) and
32 | [@jayjun](https://github.com/jayjun)!
33 |
34 | Reduced memory footprint of idle Mojito pools by forcing GC after
35 | requests complete. Thanks for the reports,
36 | [@axelson](https://github.com/axelson) and
37 | [@hubertlepicki](https://github.com/hubertlepicki)!
38 |
39 | ## 0.7.5 (2020-11-06)
40 |
41 | Fixed packaging bug in 0.7.4.
42 |
43 | ## 0.7.4 (2020-11-02)
44 |
45 | Fixed handling of Mint error responses.
46 | Thanks, [@alexandremcosta](https://github.com/alexandremcosta)!
47 |
48 | Fixed a Dialyzer warning around keyword lists.
49 | Thanks, [@Vaysman](https://github.com/Vaysman)!
50 |
51 | ## 0.7.3 (2020-06-22)
52 |
53 | Moved core Mojito functions into separate `Mojito.Base` module for
54 | easier interoperation with mocking libraries like Mox. Thanks,
55 | [@bcardarella](https://github.com/bcardarella)!
56 |
57 | ## 0.7.2 (2020-06-19)
58 |
59 | Fixed typespecs.
60 |
61 | ## 0.7.1 (2020-06-17)
62 |
63 | Fixed bug where Mojito failed to correctly handle responses with
64 | a `connection: close` header. Thanks,
65 | [@bmteller](https://github.com/bmteller)!
66 |
67 | ## 0.7.0 (2020-06-17)
68 |
69 | Added the `:max_body_size` option, to prevent a response body from
70 | growing too large. Thanks, [@rozap](https://github.com/rozap)!
71 |
72 | ## 0.6.4 (2020-05-20)
73 |
74 | Fixed bug where sending an empty string request body would hang certain
75 | HTTP/2 requests. Thanks for the report,
76 | [@Overbryd](https://github.com/Overbryd)!
77 |
78 | ## 0.6.3 (2020-03-17)
79 |
80 | `gzip`ped or `deflate`d responses are automatically expanded by
81 | Mojito. Thanks, [@mogorman](https://github.com/mogorman)!
82 |
83 | The Freedom Formatter has been removed. `mix format` is now applied.
84 |
85 | ## 0.6.2 (2020-03-11)
86 |
87 | Header values are now stringified on their way to Mint. Thanks,
88 | [@egze](https://github.com/egze)!
89 |
90 | Timeouts of `:infinity` are now supported. Thanks,
91 | [@t8rsalad](https://github.com/t8rsalad)!
92 |
93 | ## 0.6.1 (2019-12-20)
94 |
95 | Internal refactor to support different pool implementations. No features
96 | were added or changed.
97 |
98 | Code formatting improvements in docs. Thanks,
99 | [@sotojuan](https://github.com/sotojuan)!
100 |
101 | ## 0.6.0 (2019-11-02)
102 |
103 | Upgraded to Mint 1.0. Thanks, [@esvinson](https://github.com/esvinson)!
104 |
105 | Fixed typo in CHANGELOG. Thanks, [@alappe](https://github.com/alappe)!
106 |
107 | ## 0.5.0 (2019-08-21)
108 |
109 | Fixed bug where timed-out responses could arrive in connection with
110 | the next request from that caller. Thanks for the report and the
111 | test case, [@seanedwards](https://github.com/seanedwards)!
112 |
113 | Refactored to use `%Mojito.Request{}` structs more consistently across
114 | internal Mojito functions.
115 |
116 | ## 0.4.0 (2019-08-13)
117 |
118 | Upgraded to Mint 0.4.0.
119 |
120 | Requests are automatically retried when we attempt to reuse a closed
121 | connection.
122 |
123 | Added `Mojito.Headers.auth_header/2` helper for formintg HTTP Basic
124 | `Authorization` header.
125 |
126 | Don't pass the URL fragment to Mint when making requests.
127 | Thanks [@alappe](https://github.com/alappe)!
128 |
129 | Improved examples and docs around making POST requests.
130 | Thanks [@hubertlepicki](https://github.com/hubertlepicki)!
131 |
132 | Removed noisy debug output.
133 | Thanks for the report, [@bcardarella](https://github.com/bcardarella)!
134 |
135 | ## 0.3.0 (2019-05-08)
136 |
137 | Major refactor.
138 |
139 | All end-user requests pass through `Mojito.request/1`, which now
140 | accepts keyword list input as well. `Mojito.request/5` remains
141 | as an alias, and convenience methods for `get/3`, `post/4`, `put/4`,
142 | `patch/4`, `delete/3`, `head/3`, and `options/3` have been added
143 | (thanks, [@danhuynhdev](https://github.com/danhuynhdev)!).
144 |
145 | Connection pools are handled automatically, sorting requests to the
146 | correct pools, starting pools when necessary, and maintaining
147 | multiple redundant pools for GenServer efficiency.
148 |
149 | ## 0.2.2 (2019-04-26)
150 |
151 | Fixed a bug where long requests could exceed the given timeout without
152 | failing (#17). Thanks for the report,
153 | [@mischov](https://github.com/mischov)!
154 |
155 | Improved documentation about receiving `:tcp` and `:ssl` messages.
156 | Thanks for the report,
157 | [@axelson](https://github.com/axelson)!
158 |
159 | Removed an extra `Task` process creation in `Mojito.Pool.request/2`.
160 |
161 | ## 0.2.1 (2019-04-23)
162 |
163 | Refactored `Mojito.request/5` so it doesn't spawn a process. Now all
164 | TCP messages are handled within the caller process.
165 |
166 | Added `Mojito.request/1` and `Mojito.Pool.request/2`, which accept a
167 | `%Mojito.Request{}` struct as input.
168 |
169 | Removed dependency on Fuzzyurl in favor of built-in URI module.
170 |
171 | ## 0.2.0 (2019-04-19)
172 |
173 | Messages sent by Mojito now contain a `:mojito_response` prefix, to allow
174 | processes to select or ignore these messages with `receive`.
175 | Thanks [@AnilRedshift](https://github.com/AnilRedshift)!
176 |
177 | Upgraded to Mint 0.2.0.
178 |
179 | ## 0.1.1 (2019-03-28)
180 |
181 | `request/5` emits better error messages when confronted with nil or blank
182 | method or url. Thanks [@AnilRedshift](https://github.com/AnilRedshift)!
183 |
184 | ## 0.1.0 (2019-02-25)
185 |
186 | Initial release, based on [Mint](https://github.com/ericmj/mint) 0.1.0.
187 |
188 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License
2 |
3 | Copyright 2018-2021 Appcues, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a
6 | copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included
14 | in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Mojito [](https://circleci.com/gh/appcues/mojito) [](https://hexdocs.pm/mojito/Mojito.html) [](https://hex.pm/packages/mojito) [](https://github.com/appcues/mojito/blob/master/LICENSE.md)
4 |
5 | ## Now Deprecated
6 |
7 | We recommend that you use [Finch](https://github.com/sneako/finch) which
8 | is also built on [Mint](https://github.com/ericmj/mint). The creator
9 | of Finch has an [excellent writeup here](https://elixirforum.com/t/mint-vs-finch-vs-gun-vs-tesla-vs-httpoison-etc/38588/11)
10 | describing the problems with Mojito, and as a result we use Finch
11 | internally at Appcues now.
12 |
13 | ## Original Description
14 |
15 |
16 |
17 | Mojito is an easy-to-use, high-performance HTTP client built using the
18 | low-level [Mint library](https://github.com/ericmj/mint).
19 |
20 | Mojito is built for comfort _and_ for speed. Behind a simple and
21 | predictable interface, there is a sophisticated connection pool manager
22 | that delivers maximum throughput with no intervention from the user.
23 |
24 | Just want to make one request and bail? No problem. Mojito can make
25 | one-off requests as well, using the same process-less architecture as
26 | Mint.
27 |
28 | ## Quickstart
29 |
30 | ```elixir
31 | {:ok, response} = Mojito.request(method: :get, url: "https://github.com")
32 | ```
33 |
34 | ## Why Mojito?
35 |
36 | Mojito addresses the following design goals:
37 |
38 | * _Little or no configuration needed._ Use Mojito to make requests to as
39 | many different destinations as you like, without thinking about
40 | starting or selecting connection pools. Other clients like
41 | [Hackney](https://github.com/benoitc/hackney)
42 | (and [HTTPoison](https://github.com/edgurgel/httpoison)),
43 | [Ibrowse](https://github.com/cmullaparthi/ibrowse) (and
44 | [HTTPotion](https://github.com/myfreeweb/httpotion)), and
45 | Erlang's built-in [httpc](http://erlang.org/doc/man/httpc.html)
46 | offer this feature, except that...
47 |
48 | * _Connection pools should be used only for a single destination._
49 | Using a pool for making requests against multiple destinations is less
50 | than ideal, as many of the connections need to be reset before use.
51 | Mojito assigns requests to the correct pools transparently to the user.
52 | Other clients, such as [Buoy](https://github.com/lpgauth/buoy), Hackney/
53 | HTTPoison, Ibrowse/HTTPotion, etc. force the user to handle this
54 | themselves, which is often inconvenient if the full set of HTTP
55 | destinations is not known at compile time.
56 |
57 | * _Redundant pools to reduce GenServer-related bottlenecks._ Mojito can
58 | serve requests to the same destination from more than one connection
59 | pool, and those pools can be selected by round-robin at runtime in order
60 | to minimize resource contention in the Erlang VM. This feature is
61 | unique to Mojito.
62 |
63 | ## Installation
64 |
65 | Add `mojito` to your deps in `mix.exs`:
66 |
67 | ```elixir
68 | {:mojito, "~> 0.7.10"}
69 | ```
70 |
71 | ## Configuration
72 |
73 | The following `config.exs` config parameters are supported:
74 |
75 | * `:timeout` (milliseconds, default 5000) -- Default request timeout.
76 | * `:max_body_size` - Max body size in bytes. Defaults to nil in which
77 | case no max size will be enforced.
78 | * `:transport_opts` (`t:Keyword.t`, default `[]`) -- Options to pass to
79 | the `:gen_tcp` or `:ssl` modules. Commonly used to make HTTPS requests
80 | with self-signed TLS server certificates; see below for details.
81 | * `:pool_opts` (`t:pool_opts`, default `[]`) -- Configuration options
82 | for connection pools.
83 |
84 | The following `:pool_opts` options are supported:
85 |
86 | * `:size` (integer) sets the number of steady-state connections per pool.
87 | Default is 5.
88 | * `:max_overflow` (integer) sets the number of additional connections
89 | per pool, opened under conditions of heavy load.
90 | Default is 10.
91 | * `:pools` (integer) sets the maximum number of pools to open for a
92 | single destination host and port (not the maximum number of total
93 | pools to open). Default is 5.
94 | * `:strategy` is either `:lifo` or `:fifo`, and selects which connection
95 | should be checked out of a single pool. Default is `:lifo`.
96 | * `:destinations` (keyword list of `t:pool_opts`) allows these parameters
97 | to be set for individual `:"host:port"` destinations.
98 |
99 | For example:
100 |
101 | ```elixir
102 | use Mix.Config
103 |
104 | config :mojito,
105 | timeout: 2500,
106 | pool_opts: [
107 | size: 10,
108 | destinations: [
109 | "example.com:443": [
110 | size: 20,
111 | max_overflow: 20,
112 | pools: 10
113 | ]
114 | ]
115 | ]
116 | ```
117 |
118 | Certain configs can be overridden with each request. See `request/1`.
119 |
120 | ## Usage
121 |
122 | Make requests with `Mojito.request/1` or `Mojito.request/5`:
123 |
124 | ```elixir
125 | >>>> Mojito.request(:get, "https://jsonplaceholder.typicode.com/posts/1")
126 | ## or...
127 | >>>> Mojito.request(%{method: :get, url: "https://jsonplaceholder.typicode.com/posts/1"})
128 | ## or...
129 | >>>> Mojito.request(method: :get, url: "https://jsonplaceholder.typicode.com/posts/1")
130 |
131 | {:ok,
132 | %Mojito.Response{
133 | body: "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"sunt aut facere repellat provident occaecati excepturi optio reprehenderit\",\n \"body\": \"quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto\"\n}",
134 | headers: [
135 | {"content-type", "application/json; charset=utf-8"},
136 | {"content-length", "292"},
137 | {"connection", "keep-alive"},
138 | ...
139 | ],
140 | status_code: 200
141 | }}
142 | ```
143 |
144 | By default, Mojito will use a connection pool for requests, automatically
145 | handling the creation and reuse of pools. If this is not desired,
146 | specify the `pool: false` option with a request to perform a one-off request.
147 | See the documentation for `request/1` for more details.
148 |
149 | ## Self-signed SSL/TLS certificates
150 |
151 | To accept self-signed certificates in HTTPS connections, you can give the
152 | `transport_opts: [verify: :verify_none]` option to `Mojito.request`
153 | or `Mojito.Pool.request`:
154 |
155 | ```elixir
156 | >>>> Mojito.request(method: :get, url: "https://localhost:8443/")
157 | {:error, {:tls_alert, 'bad certificate'}}
158 |
159 | >>>> Mojito.request(method: :get, url: "https://localhost:8443/", opts: [transport_opts: [verify: :verify_none]])
160 | {:ok, %Mojito.Response{ ... }}
161 | ```
162 |
163 | ## Telemetry
164 |
165 | Mojito integrates with the standard
166 | [Telemetry](https://github.com/beam-telemetry/telemetry) library.
167 |
168 | See the [Mojito.Telemetry](https://github.com/appcues/mojito/blob/master/lib/mojito/telemetry.ex)
169 | module for more information.
170 |
171 |
172 |
173 | ## Changelog
174 |
175 | See the [CHANGELOG.md](https://github.com/appcues/mojito/blob/master/CHANGELOG.md).
176 |
177 | ## Contributing
178 |
179 | Thanks for considering contributing to this project, and to the free
180 | software ecosystem at large!
181 |
182 | Interested in contributing a bug report? Terrific! Please open a [GitHub
183 | issue](https://github.com/appcues/mojito/issues) and include as much detail
184 | as you can. If you have a solution, even better -- please open a pull
185 | request with a clear description and tests.
186 |
187 | Have a feature idea? Excellent! Please open a [GitHub
188 | issue](https://github.com/appcues/mojito/issues) for discussion.
189 |
190 | Want to implement an issue that's been discussed? Fantastic! Please
191 | open a [GitHub pull request](https://github.com/appcues/mojito/pulls)
192 | and write a clear description of the patch.
193 | We'll merge your PR a lot sooner if it is well-documented and fully
194 | tested.
195 |
196 | Contributors and contributions are listed in the
197 | [changelog](https://github.com/appcues/mojito/blob/master/CHANGELOG.md).
198 | Heartfelt thanks to everyone who's helped make Mojito better.
199 |
200 | ## Copyright and License
201 |
202 | Copyright 2018-2021 Appcues, Inc.
203 |
204 | This software is released under the [MIT License](./LICENSE.md).
205 |
--------------------------------------------------------------------------------
/assets/mojito-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appcues/mojito/ca2a99d0810933dfba8a6ad088439425956f6aff/assets/mojito-full.png
--------------------------------------------------------------------------------
/assets/mojito.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appcues/mojito/ca2a99d0810933dfba8a6ad088439425956f6aff/assets/mojito.png
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | use Mix.Config
4 |
5 | # This configuration is loaded before any dependency and is restricted
6 | # to this project. If another project depends on this project, this
7 | # file won't be loaded nor affect the parent project. For this reason,
8 | # if you want to provide default values for your application for
9 | # 3rd-party users, it should be done in your "mix.exs" file.
10 |
11 | # You can configure your application as:
12 | #
13 | # config :xhttp_client, key: :value
14 | #
15 | # and access this configuration in your application as:
16 | #
17 | # Application.get_env(:xhttp_client, :key)
18 | #
19 | # You can also configure a 3rd-party app:
20 | #
21 | # config :logger, level: :info
22 | #
23 |
24 | # It is also possible to import configuration files, relative to this
25 | # directory. For example, you can emulate configuration per environment
26 | # by uncommenting the line below and defining dev.exs, test.exs and such.
27 | # Configuration from the imported file will override the ones defined
28 | # here (which is why it is important to import them last).
29 | #
30 | import_config "#{Mix.env()}*.exs"
31 |
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :mojito,
4 | test_server_http_port: 18999,
5 | test_server_https_port: 18443,
6 | pool_opts: [
7 | size: 2,
8 | max_overflow: 2,
9 | pools: 5,
10 | destinations: [
11 | "localhost:18443": [
12 | pools: 10
13 | ]
14 | ]
15 | ]
16 |
--------------------------------------------------------------------------------
/lib/mojito.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito do
2 | @external_resource "README.md"
3 | @moduledoc File.read!("README.md")
4 | |> String.split(~r//)
5 | |> Enum.fetch!(1)
6 |
7 | use Mojito.Base
8 | end
9 |
--------------------------------------------------------------------------------
/lib/mojito/application.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Application do
2 | @moduledoc false
3 |
4 | use Application
5 |
6 | def start(_type, _args) do
7 | children = [
8 | Mojito.Pool.Poolboy.Manager,
9 | {Registry,
10 | keys: :duplicate,
11 | name: Mojito.Pool.Poolboy.Registry,
12 | partitions: System.schedulers_online()}
13 | ]
14 |
15 | opts = [strategy: :one_for_one, name: Mojito.Supervisor]
16 | Supervisor.start_link(children, opts)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/mojito/base.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Base do
2 | @moduledoc ~S"""
3 | Provides a default implementation for Mojito functions.
4 |
5 | This module is meant to be `use`'d in custom modules in order to wrap the
6 | functionalities provided by Mojiti. For example, this is very useful to
7 | build custom API clients around Mojito:
8 |
9 | defmodule CustomAPI do
10 | use Mojito.Base
11 | end
12 |
13 | """
14 |
15 | @type method ::
16 | :head
17 | | :get
18 | | :post
19 | | :put
20 | | :patch
21 | | :delete
22 | | :options
23 | | String.t()
24 |
25 | @type header :: {String.t(), String.t()}
26 |
27 | @type headers :: [header]
28 |
29 | @type request :: %Mojito.Request{
30 | method: method,
31 | url: String.t(),
32 | headers: headers | nil,
33 | body: String.t() | nil,
34 | opts: Keyword.t() | nil
35 | }
36 |
37 | @type request_kwlist :: [request_field]
38 |
39 | @type request_field ::
40 | {:method, method}
41 | | {:url, String.t()}
42 | | {:headers, headers}
43 | | {:body, String.t()}
44 | | {:opts, Keyword.t()}
45 |
46 | @type response :: %Mojito.Response{
47 | status_code: pos_integer,
48 | headers: headers,
49 | body: String.t(),
50 | complete: boolean
51 | }
52 |
53 | @type error :: %Mojito.Error{
54 | reason: any,
55 | message: String.t() | nil
56 | }
57 |
58 | @type pool_opts :: [pool_opt | {:destinations, [{atom, pool_opts}]}]
59 |
60 | @type pool_opt ::
61 | {:size, pos_integer}
62 | | {:max_overflow, non_neg_integer}
63 | | {:pools, pos_integer}
64 | | {:strategy, :lifo | :fifo}
65 |
66 | @type url :: String.t()
67 | @type body :: String.t()
68 | @type payload :: String.t()
69 |
70 | @callback request(method, url) ::
71 | {:ok, response} | {:error, error} | no_return
72 | @callback request(method, url, headers) ::
73 | {:ok, response} | {:error, error} | no_return
74 | @callback request(method, url, headers, body) ::
75 | {:ok, response} | {:error, error} | no_return
76 | @callback request(method, url, headers, body, Keyword.t()) ::
77 | {:ok, response} | {:error, error} | no_return
78 | @callback request(request | request_kwlist) ::
79 | {:ok, response} | {:error, error}
80 |
81 | @callback head(url) :: {:ok, response} | {:error, error} | no_return
82 | @callback head(url, headers) :: {:ok, response} | {:error, error} | no_return
83 | @callback head(url, headers, Keyword.t()) ::
84 | {:ok, response} | {:error, error} | no_return
85 |
86 | @callback get(url) :: {:ok, response} | {:error, error} | no_return
87 | @callback get(url, headers) :: {:ok, response} | {:error, error} | no_return
88 | @callback get(url, headers, Keyword.t()) ::
89 | {:ok, response} | {:error, error} | no_return
90 |
91 | @callback post(url) :: {:ok, response} | {:error, error} | no_return
92 | @callback post(url, headers) :: {:ok, response} | {:error, error} | no_return
93 | @callback post(url, headers, payload) ::
94 | {:ok, response} | {:error, error} | no_return
95 | @callback post(url, headers, payload, Keyword.t()) ::
96 | {:ok, response} | {:error, error} | no_return
97 |
98 | @callback put(url) :: {:ok, response} | {:error, error} | no_return
99 | @callback put(url, headers) :: {:ok, response} | {:error, error} | no_return
100 | @callback put(url, headers, payload) ::
101 | {:ok, response} | {:error, error} | no_return
102 | @callback put(url, headers, payload, Keyword.t()) ::
103 | {:ok, response} | {:error, error} | no_return
104 |
105 | @callback patch(url) :: {:ok, response} | {:error, error} | no_return
106 | @callback patch(url, headers) :: {:ok, response} | {:error, error} | no_return
107 | @callback patch(url, headers, payload) ::
108 | {:ok, response} | {:error, error} | no_return
109 | @callback patch(url, headers, payload, Keyword.t()) ::
110 | {:ok, response} | {:error, error} | no_return
111 |
112 | @callback delete(url) :: {:ok, response} | {:error, error} | no_return
113 | @callback delete(url, headers) ::
114 | {:ok, response} | {:error, error} | no_return
115 | @callback delete(url, headers, Keyword.t()) ::
116 | {:ok, response} | {:error, error} | no_return
117 |
118 | @callback options(url) :: {:ok, response} | {:error, error} | no_return
119 | @callback options(url, headers) ::
120 | {:ok, response} | {:error, error} | no_return
121 | @callback options(url, headers, Keyword.t()) ::
122 | {:ok, response} | {:error, error} | no_return
123 |
124 | defmacro __using__(_) do
125 | quote do
126 | @behaviour Mojito.Base
127 |
128 | @type method :: Mojito.Base.method()
129 | @type header :: Mojito.Base.header()
130 | @type headers :: Mojito.Base.headers()
131 | @type request :: Mojito.Base.request()
132 | @type request_kwlist :: Mojito.Base.request_kwlist()
133 | @type request_fields :: Mojito.Base.request_field()
134 | @type response :: Mojito.Base.response()
135 | @type error :: Mojito.Base.error()
136 | @type pool_opts :: Mojito.Base.pool_opts()
137 | @type pool_opt :: Mojito.Base.pool_opt()
138 | @type url :: Mojito.Base.url()
139 | @type body :: Mojito.Base.body()
140 | @type payload :: Mojito.Base.payload()
141 |
142 | @doc ~S"""
143 | Performs an HTTP request and returns the response.
144 |
145 | See `request/1` for details.
146 | """
147 | @spec request(method, url, headers, body | nil, Keyword.t()) ::
148 | {:ok, response} | {:error, error} | no_return
149 | def request(method, url, headers \\ [], body \\ "", opts \\ []) do
150 | %Mojito.Request{
151 | method: method,
152 | url: url,
153 | headers: headers,
154 | body: body,
155 | opts: opts
156 | }
157 | |> request
158 | end
159 |
160 | @doc ~S"""
161 | Performs an HTTP request and returns the response.
162 |
163 | If the `pool: true` option is given, or `:pool` is not specified, the
164 | request will be made using Mojito's automatic connection pooling system.
165 | For more details, see `Mojito.Pool.request/1`. This is the default
166 | mode of operation, and is recommended for best performance.
167 |
168 | If `pool: false` is given as an option, the request will be made on
169 | a brand new connection. This does not spawn an additional process.
170 | Messages of the form `{:tcp, _, _}` or `{:ssl, _, _}` will be sent to
171 | and handled by the caller. If the caller process expects to receive
172 | other `:tcp` or `:ssl` messages at the same time, conflicts can occur;
173 | in this case, it is recommended to wrap `request/1` in `Task.async/1`,
174 | or use one of the pooled request modes.
175 |
176 | Options:
177 |
178 | * `:pool` - See above.
179 | * `:timeout` - Response timeout in milliseconds, or `:infinity`.
180 | Defaults to `Application.get_env(:mojito, :timeout, 5000)`.
181 | * `:raw` - Set this to `true` to prevent the decompression of
182 | `gzip` or `compress`-encoded responses.
183 | * `:transport_opts` - Options to be passed to either `:gen_tcp` or `:ssl`.
184 | Most commonly used to perform insecure HTTPS requests via
185 | `transport_opts: [verify: :verify_none]`.
186 | """
187 | @spec request(request | request_kwlist) ::
188 | {:ok, response} | {:error, error}
189 | def request(request) do
190 | with {:ok, valid_request} <- Mojito.Request.validate_request(request),
191 | {:ok, valid_request} <-
192 | Mojito.Request.convert_headers_values_to_string(valid_request) do
193 | case Keyword.get(valid_request.opts, :pool, true) do
194 | true ->
195 | Mojito.Pool.Poolboy.request(valid_request)
196 |
197 | false ->
198 | Mojito.Request.Single.request(valid_request)
199 |
200 | pid when is_pid(pid) ->
201 | Mojito.Pool.Poolboy.Single.request(pid, valid_request)
202 |
203 | impl when is_atom(impl) ->
204 | impl.request(valid_request)
205 | end
206 | |> maybe_decompress(valid_request.opts)
207 | end
208 | end
209 |
210 | defp maybe_decompress({:ok, response}, opts) do
211 | case Keyword.get(opts, :raw) do
212 | true ->
213 | {:ok, response}
214 |
215 | _ ->
216 | case Enum.find(response.headers, fn {k, _v} ->
217 | k == "content-encoding"
218 | end) do
219 | {"content-encoding", "gzip"} ->
220 | {:ok,
221 | %Mojito.Response{response | body: :zlib.gunzip(response.body)}}
222 |
223 | {"content-encoding", "deflate"} ->
224 | {:ok,
225 | %Mojito.Response{
226 | response
227 | | body: :zlib.uncompress(response.body)
228 | }}
229 |
230 | _ ->
231 | # we don't have a decompressor for this so just returning
232 | {:ok, response}
233 | end
234 | end
235 | end
236 |
237 | defp maybe_decompress(response, _opts) do
238 | response
239 | end
240 |
241 | @doc ~S"""
242 | Performs an HTTP HEAD request and returns the response.
243 |
244 | See `request/1` for documentation.
245 | """
246 | @spec head(url, headers, Keyword.t()) ::
247 | {:ok, response} | {:error, error} | no_return
248 | def head(url, headers \\ [], opts \\ []) do
249 | request(:head, url, headers, nil, opts)
250 | end
251 |
252 | @doc ~S"""
253 | Performs an HTTP GET request and returns the response.
254 |
255 | ## Examples
256 |
257 | Assemble a URL with a query string params and fetch it with GET request:
258 |
259 | >>>> "https://www.google.com/search"
260 | ...> |> URI.parse()
261 | ...> |> Map.put(:query, URI.encode_query(%{"q" => "mojito elixir"}))
262 | ...> |> URI.to_string()
263 | ...> |> Mojito.get()
264 | {:ok,
265 | %Mojito.Response{
266 | body: " ...",
267 | complete: true,
268 | headers: [
269 | {"content-type", "text/html; charset=ISO-8859-1"},
270 | ...
271 | ],
272 | status_code: 200
273 | }}
274 |
275 |
276 | See `request/1` for detailed documentation.
277 | """
278 | @spec get(url, headers, Keyword.t()) ::
279 | {:ok, response} | {:error, error} | no_return
280 | def get(url, headers \\ [], opts \\ []) do
281 | request(:get, url, headers, nil, opts)
282 | end
283 |
284 | @doc ~S"""
285 | Performs an HTTP POST request and returns the response.
286 |
287 | ## Examples
288 |
289 | Submitting a form with POST request:
290 |
291 | >>>> Mojito.post(
292 | ...> "http://localhost:4000/messages",
293 | ...> [{"content-type", "application/x-www-form-urlencoded"}],
294 | ...> URI.encode_query(%{"message[subject]" => "Contact request", "message[content]" => "data"}))
295 | {:ok,
296 | %Mojito.Response{
297 | body: "Thank you!",
298 | complete: true,
299 | headers: [
300 | {"server", "Cowboy"},
301 | {"connection", "keep-alive"},
302 | ...
303 | ],
304 | status_code: 200
305 | }}
306 |
307 | Submitting a JSON payload as POST request body:
308 |
309 | >>>> Mojito.post(
310 | ...> "http://localhost:4000/api/messages",
311 | ...> [{"content-type", "application/json"}],
312 | ...> Jason.encode!(%{"message" => %{"subject" => "Contact request", "content" => "data"}}))
313 | {:ok,
314 | %Mojito.Response{
315 | body: "{\"message\": \"Thank you!\"}",
316 | complete: true,
317 | headers: [
318 | {"server", "Cowboy"},
319 | {"connection", "keep-alive"},
320 | ...
321 | ],
322 | status_code: 200
323 | }}
324 |
325 | See `request/1` for detailed documentation.
326 | """
327 | @spec post(url, headers, payload, Keyword.t()) ::
328 | {:ok, response} | {:error, error} | no_return
329 | def post(url, headers \\ [], payload \\ "", opts \\ []) do
330 | request(:post, url, headers, payload, opts)
331 | end
332 |
333 | @doc ~S"""
334 | Performs an HTTP PUT request and returns the response.
335 |
336 | See `request/1` and `post/4` for documentation and examples.
337 | """
338 | @spec put(url, headers, payload, Keyword.t()) ::
339 | {:ok, response} | {:error, error} | no_return
340 | def put(url, headers \\ [], payload \\ "", opts \\ []) do
341 | request(:put, url, headers, payload, opts)
342 | end
343 |
344 | @doc ~S"""
345 | Performs an HTTP PATCH request and returns the response.
346 |
347 | See `request/1` and `post/4` for documentation and examples.
348 | """
349 | @spec patch(url, headers, payload, Keyword.t()) ::
350 | {:ok, response} | {:error, error} | no_return
351 | def patch(url, headers \\ [], payload \\ "", opts \\ []) do
352 | request(:patch, url, headers, payload, opts)
353 | end
354 |
355 | @doc ~S"""
356 | Performs an HTTP DELETE request and returns the response.
357 |
358 | See `request/1` for documentation and examples.
359 | """
360 | @spec delete(url, headers, Keyword.t()) ::
361 | {:ok, response} | {:error, error} | no_return
362 | def delete(url, headers \\ [], opts \\ []) do
363 | request(:delete, url, headers, nil, opts)
364 | end
365 |
366 | @doc ~S"""
367 | Performs an HTTP OPTIONS request and returns the response.
368 |
369 | See `request/1` for documentation.
370 | """
371 | @spec options(url, headers, Keyword.t()) ::
372 | {:ok, response} | {:error, error} | no_return
373 | def options(url, headers \\ [], opts \\ []) do
374 | request(:options, url, headers, nil, opts)
375 | end
376 | end
377 | end
378 | end
379 |
--------------------------------------------------------------------------------
/lib/mojito/config.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Config do
2 | @moduledoc false
3 |
4 | def timeout do
5 | Application.get_env(:mojito, :timeout, 5000)
6 | end
7 | end
8 |
9 | ## pool_opts are handled in Mojito.Pool
10 |
--------------------------------------------------------------------------------
/lib/mojito/conn.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Conn do
2 | @moduledoc false
3 |
4 | alias Mojito.{Error, Telemetry, Utils}
5 |
6 | defstruct conn: nil,
7 | protocol: nil,
8 | hostname: nil,
9 | port: nil
10 |
11 | @type t :: %Mojito.Conn{}
12 |
13 | @doc ~S"""
14 | Connects to the specified endpoint, returning a connection to the server.
15 | No requests are made.
16 | """
17 | @spec connect(String.t(), Keyword.t()) :: {:ok, t} | {:error, any}
18 | def connect(url, opts \\ []) do
19 | with {:ok, protocol, hostname, port} <- Utils.decompose_url(url) do
20 | connect(protocol, hostname, port, opts)
21 | end
22 | end
23 |
24 | @doc ~S"""
25 | Closes a connection
26 | """
27 | @spec close(t) :: :ok
28 | def close(conn) do
29 | Mint.HTTP.close(conn.conn)
30 | :ok
31 | end
32 |
33 | @doc ~S"""
34 | Connects to the server specified in the given URL,
35 | returning a connection to the server. No requests are made.
36 | """
37 | @spec connect(String.t(), String.t(), non_neg_integer, Keyword.t()) ::
38 | {:ok, t} | {:error, any}
39 | def connect(protocol, hostname, port, opts \\ []) do
40 | with meta <- %{host: hostname, port: port},
41 | start_time <- Telemetry.start(:connect, meta),
42 | {:ok, proto} <- protocol_to_atom(protocol),
43 | {:ok, mint_conn} <- Mint.HTTP.connect(proto, hostname, port, opts) do
44 | Telemetry.stop(:connect, start_time, meta)
45 |
46 | {:ok,
47 | %Mojito.Conn{
48 | conn: mint_conn,
49 | protocol: proto,
50 | hostname: hostname,
51 | port: port
52 | }}
53 | end
54 | end
55 |
56 | defp protocol_to_atom("http"), do: {:ok, :http}
57 | defp protocol_to_atom("https"), do: {:ok, :https}
58 | defp protocol_to_atom(:http), do: {:ok, :http}
59 | defp protocol_to_atom(:https), do: {:ok, :https}
60 |
61 | defp protocol_to_atom(proto),
62 | do: {:error, %Error{message: "bad protocol #{inspect(proto)}"}}
63 |
64 | @doc ~S"""
65 | Initiates a request on the given connection. Returns the updated Conn and
66 | a reference to this request (which is required when receiving pipelined
67 | responses).
68 | """
69 | @spec request(t, Mojito.request()) :: {:ok, t, reference} | {:error, any}
70 | def request(conn, request) do
71 | max_body_size = request.opts[:max_body_size]
72 | response = %Mojito.Response{body: [], size: max_body_size}
73 |
74 | with {:ok, relative_url, auth_headers} <-
75 | Utils.get_relative_url_and_auth_headers(request.url),
76 | {:ok, mint_conn, request_ref} <-
77 | Mint.HTTP.request(
78 | conn.conn,
79 | method_to_string(request.method),
80 | relative_url,
81 | auth_headers ++ request.headers,
82 | :stream
83 | ),
84 | {:ok, mint_conn, response} <-
85 | stream_request_body(mint_conn, request_ref, response, request.body) do
86 | {:ok, %{conn | conn: mint_conn}, request_ref, response}
87 | end
88 | end
89 |
90 | defp stream_request_body(mint_conn, request_ref, response, nil) do
91 | stream_request_body(mint_conn, request_ref, response, "")
92 | end
93 |
94 | defp stream_request_body(mint_conn, request_ref, response, "") do
95 | with {:ok, mint_conn} <-
96 | Mint.HTTP.stream_request_body(mint_conn, request_ref, :eof) do
97 | {:ok, mint_conn, response}
98 | end
99 | end
100 |
101 | defp stream_request_body(
102 | %Mint.HTTP1{} = mint_conn,
103 | request_ref,
104 | response,
105 | body
106 | ) do
107 | {chunk, rest} = split_chunk(body, 65_535)
108 |
109 | with {:ok, mint_conn} <-
110 | Mint.HTTP.stream_request_body(mint_conn, request_ref, chunk) do
111 | stream_request_body(mint_conn, request_ref, response, rest)
112 | end
113 | end
114 |
115 | defp stream_request_body(
116 | %Mint.HTTP2{} = mint_conn,
117 | request_ref,
118 | response,
119 | body
120 | ) do
121 | chunk_size =
122 | min(
123 | Mint.HTTP2.get_window_size(mint_conn, {:request, request_ref}),
124 | Mint.HTTP2.get_window_size(mint_conn, :connection)
125 | )
126 |
127 | {chunk, rest} = split_chunk(body, chunk_size)
128 |
129 | with {:ok, mint_conn} <-
130 | Mint.HTTP.stream_request_body(mint_conn, request_ref, chunk) do
131 | {mint_conn, response} =
132 | if is_nil(rest) do
133 | {mint_conn, response}
134 | else
135 | {:ok, mint_conn, resps} =
136 | receive do
137 | msg -> Mint.HTTP.stream(mint_conn, msg)
138 | end
139 |
140 | {:ok, response} = Mojito.Response.apply_resps(response, resps)
141 |
142 | {mint_conn, response}
143 | end
144 |
145 | if response.complete do
146 | {:ok, mint_conn, response}
147 | else
148 | stream_request_body(mint_conn, request_ref, response, rest)
149 | end
150 | end
151 | end
152 |
153 | defp method_to_string(m) when is_atom(m) do
154 | m |> to_string |> String.upcase()
155 | end
156 |
157 | defp method_to_string(m) when is_binary(m) do
158 | m |> String.upcase()
159 | end
160 |
161 | defp split_chunk(binary, chunk_size)
162 | when is_binary(binary) and is_integer(chunk_size) do
163 | case binary do
164 | <> ->
165 | {chunk, rest}
166 |
167 | _ ->
168 | {binary, nil}
169 | end
170 | end
171 | end
172 |
--------------------------------------------------------------------------------
/lib/mojito/conn_server.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.ConnServer do
2 | @moduledoc false
3 |
4 | use GenServer
5 | require Logger
6 |
7 | alias Mojito.{Conn, Response, Utils}
8 |
9 | @type state :: map
10 |
11 | @doc ~S"""
12 | Starts a `Mojito.ConnServer`.
13 |
14 | `Mojito.ConnServer` is a GenServer that handles a single
15 | `Mojito.Conn`. It supports automatic reconnection,
16 | connection keep-alive, and request pipelining.
17 |
18 | It's intended for usage through `Mojito.Pool`.
19 |
20 | Example:
21 |
22 | {:ok, pid} = Mojito.ConnServer.start_link()
23 | :ok = GenServer.cast(pid, {:request, self(), :get, "http://example.com", [], "", []})
24 | receive do
25 | {:ok, response} -> response
26 | after
27 | 1_000 -> :timeout
28 | end
29 | """
30 | @spec start_link(Keyword.t()) :: {:ok, pid} | {:error, any}
31 | def start_link(args \\ []) do
32 | GenServer.start_link(__MODULE__, args)
33 | end
34 |
35 | @doc ~S"""
36 | Initiates a request. The `reply_to` pid will receive the response in a
37 | message of the format `{:ok, %Mojito.Response{}} | {:error, any}`.
38 | """
39 | @spec request(pid, Mojito.request(), pid, reference) :: :ok | {:error, any}
40 | def request(server_pid, request, reply_to, response_ref) do
41 | GenServer.call(server_pid, {:request, request, reply_to, response_ref})
42 | end
43 |
44 | #### GenServer callbacks
45 |
46 | def init(_) do
47 | {:ok,
48 | %{
49 | conn: nil,
50 | protocol: nil,
51 | hostname: nil,
52 | port: nil,
53 | responses: %{},
54 | reply_tos: %{},
55 | response_refs: %{}
56 | }}
57 | end
58 |
59 | def terminate(_reason, state) do
60 | close_connections(state)
61 | end
62 |
63 | def handle_call(
64 | {:request, request, reply_to, response_ref},
65 | _from,
66 | state
67 | ) do
68 | with {:ok, state, _request_ref} <-
69 | start_request(state, request, reply_to, response_ref) do
70 | {:reply, :ok, state}
71 | else
72 | err -> {:reply, err, close_connections(state)}
73 | end
74 | end
75 |
76 | ## `msg` is an incoming chunk of a response
77 | def handle_info(msg, state) do
78 | if !state.conn do
79 | {:noreply, close_connections(state)}
80 | else
81 | case Mint.HTTP.stream(state.conn.conn, msg) do
82 | {:ok, mint_conn, resps} ->
83 | state_conn = state.conn |> Map.put(:conn, mint_conn)
84 | state = %{state | conn: state_conn}
85 | {:noreply, apply_resps(state, resps)}
86 |
87 | {:error, _mint_conn, _error, _resps} ->
88 | {:noreply, close_connections(state)}
89 |
90 | :unknown ->
91 | {:noreply, state}
92 | end
93 | end
94 | end
95 |
96 | #### Helpers
97 |
98 | @spec close_connections(state) :: state
99 | defp close_connections(state) do
100 | Enum.each(state.reply_tos, fn {_request_ref, reply_to} ->
101 | respond(reply_to, {:error, :closed})
102 | end)
103 |
104 | %{state | conn: nil, responses: %{}, reply_tos: %{}, response_refs: %{}}
105 | end
106 |
107 | defp apply_resps(state, []), do: state
108 |
109 | defp apply_resps(state, [resp | rest]) do
110 | apply_resp(state, resp) |> apply_resps(rest)
111 | end
112 |
113 | defp apply_resp(state, {:status, request_ref, _status} = msg) do
114 | {:ok, response} =
115 | Map.get(state.responses, request_ref)
116 | |> Response.apply_resp(msg)
117 |
118 | %{state | responses: Map.put(state.responses, request_ref, response)}
119 | end
120 |
121 | defp apply_resp(state, {:headers, request_ref, _headers} = msg) do
122 | {:ok, response} =
123 | Map.get(state.responses, request_ref)
124 | |> Response.apply_resp(msg)
125 |
126 | %{state | responses: Map.put(state.responses, request_ref, response)}
127 | end
128 |
129 | defp apply_resp(state, {:data, request_ref, _chunk} = msg) do
130 | case Map.get(state.responses, request_ref) |> Response.apply_resp(msg) do
131 | {:ok, response} ->
132 | %{state | responses: Map.put(state.responses, request_ref, response)}
133 |
134 | {:error, _} = err ->
135 | halt(state, request_ref, err)
136 | end
137 | end
138 |
139 | defp apply_resp(state, {:error, request_ref, err}) do
140 | halt(state, request_ref, {:error, err})
141 | end
142 |
143 | defp apply_resp(state, {:done, request_ref}) do
144 | r = Map.get(state.responses, request_ref)
145 | body = :erlang.list_to_binary(r.body)
146 | size = byte_size(body)
147 | response = %{r | complete: true, body: body, size: size}
148 | halt(state, request_ref, {:ok, response})
149 | end
150 |
151 | defp halt(state, request_ref, response) do
152 | response_ref = state.response_refs |> Map.get(request_ref)
153 | Map.get(state.reply_tos, request_ref) |> respond(response, response_ref)
154 |
155 | %{
156 | state
157 | | responses: Map.delete(state.responses, request_ref),
158 | reply_tos: Map.delete(state.reply_tos, request_ref),
159 | response_refs: Map.delete(state.response_refs, request_ref)
160 | }
161 | end
162 |
163 | defp respond(pid, message, response_ref \\ nil) do
164 | send(pid, {:mojito_response, response_ref, message})
165 | end
166 |
167 | @spec start_request(
168 | state,
169 | Mojito.request(),
170 | pid,
171 | reference
172 | ) :: {:ok, state, reference} | {:error, any}
173 | defp start_request(state, request, reply_to, response_ref) do
174 | with {:ok, state} <- ensure_connection(state, request.url, request.opts),
175 | {:ok, conn, request_ref, response} <- Conn.request(state.conn, request) do
176 | case response do
177 | %{complete: true} ->
178 | ## Request was completed by server during stream_request_body
179 | respond(reply_to, {:ok, response}, response_ref)
180 | {:ok, %{state | conn: conn}, request_ref}
181 |
182 | _ ->
183 | responses = state.responses |> Map.put(request_ref, response)
184 | reply_tos = state.reply_tos |> Map.put(request_ref, reply_to)
185 |
186 | response_refs =
187 | state.response_refs |> Map.put(request_ref, response_ref)
188 |
189 | state = %{
190 | state
191 | | conn: conn,
192 | responses: responses,
193 | reply_tos: reply_tos,
194 | response_refs: response_refs
195 | }
196 |
197 | {:ok, state, request_ref}
198 | end
199 | end
200 | end
201 |
202 | @spec ensure_connection(state, String.t(), Keyword.t()) ::
203 | {:ok, state} | {:error, any}
204 | defp ensure_connection(state, url, opts) do
205 | with {:ok, protocol, hostname, port} <- Utils.decompose_url(url) do
206 | new_destination =
207 | state.protocol != protocol || state.hostname != hostname ||
208 | state.port != port
209 |
210 | cond do
211 | !state.conn || new_destination ->
212 | connect(state, protocol, hostname, port, opts)
213 |
214 | :else ->
215 | {:ok, state}
216 | end
217 | end
218 | end
219 |
220 | @spec connect(state, String.t(), String.t(), non_neg_integer, Keyword.t()) ::
221 | {:ok, state} | {:error, any}
222 | defp connect(state, protocol, hostname, port, opts) do
223 | with {:ok, conn} <- Mojito.Conn.connect(protocol, hostname, port, opts) do
224 | {:ok,
225 | %{state | conn: conn, protocol: protocol, hostname: hostname, port: port}}
226 | end
227 | end
228 | end
229 |
--------------------------------------------------------------------------------
/lib/mojito/error.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Error do
2 | @moduledoc false
3 |
4 | defstruct [:reason, :message]
5 |
6 | @type t :: Mojito.error()
7 | end
8 |
--------------------------------------------------------------------------------
/lib/mojito/headers.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Headers do
2 | @moduledoc ~S"""
3 | Functions for working with HTTP request and response headers, as described
4 | in the [HTTP 1.1 specification](https://www.w3.org/Protocols/rfc2616/rfc2616.html).
5 |
6 | Headers are represented in Elixir as a list of `{"header_name", "value"}`
7 | tuples. Multiple entries for the same header name are allowed.
8 |
9 | Capitalization of header names is preserved during insertion,
10 | however header names are handled case-insensitively during
11 | lookup and deletion.
12 | """
13 |
14 | @type headers :: Mojito.headers()
15 |
16 | @doc ~S"""
17 | Returns the value for the given HTTP request or response header,
18 | or `nil` if not found.
19 |
20 | Header names are matched case-insensitively.
21 |
22 | If more than one matching header is found, the values are joined with
23 | `","` as specified in [RFC 2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).
24 |
25 | Example:
26 |
27 | iex> headers = [
28 | ...> {"header1", "foo"},
29 | ...> {"header2", "bar"},
30 | ...> {"Header1", "baz"}
31 | ...> ]
32 | iex> Mojito.Headers.get(headers, "header2")
33 | "bar"
34 | iex> Mojito.Headers.get(headers, "HEADER1")
35 | "foo,baz"
36 | iex> Mojito.Headers.get(headers, "header3")
37 | nil
38 | """
39 | @spec get(headers, String.t()) :: String.t() | nil
40 | def get(headers, name) do
41 | case get_values(headers, name) do
42 | [] -> nil
43 | values -> values |> Enum.join(",")
44 | end
45 | end
46 |
47 | @doc ~S"""
48 | Returns all values for the given HTTP request or response header.
49 | Returns an empty list if none found.
50 |
51 | Header names are matched case-insensitively.
52 |
53 | Example:
54 |
55 | iex> headers = [
56 | ...> {"header1", "foo"},
57 | ...> {"header2", "bar"},
58 | ...> {"Header1", "baz"}
59 | ...> ]
60 | iex> Mojito.Headers.get_values(headers, "header2")
61 | ["bar"]
62 | iex> Mojito.Headers.get_values(headers, "HEADER1")
63 | ["foo", "baz"]
64 | iex> Mojito.Headers.get_values(headers, "header3")
65 | []
66 | """
67 | @spec get_values(headers, String.t()) :: [String.t()]
68 | def get_values(headers, name) do
69 | get_values(headers, String.downcase(name), [])
70 | end
71 |
72 | defp get_values([], _name, values), do: values
73 |
74 | defp get_values([{key, value} | rest], name, values) do
75 | new_values =
76 | if String.downcase(key) == name do
77 | values ++ [value]
78 | else
79 | values
80 | end
81 |
82 | get_values(rest, name, new_values)
83 | end
84 |
85 | @doc ~S"""
86 | Puts the given header `value` under `name`, removing any values previously
87 | stored under `name`. The new header is placed at the end of the list.
88 |
89 | Header names are matched case-insensitively, but case of `name` is preserved
90 | when adding the header.
91 |
92 | Example:
93 |
94 | iex> headers = [
95 | ...> {"header1", "foo"},
96 | ...> {"header2", "bar"},
97 | ...> {"Header1", "baz"}
98 | ...> ]
99 | iex> Mojito.Headers.put(headers, "HEADER1", "quux")
100 | [{"header2", "bar"}, {"HEADER1", "quux"}]
101 | """
102 | @spec put(headers, String.t(), String.t()) :: headers
103 | def put(headers, name, value) do
104 | delete(headers, name) ++ [{name, value}]
105 | end
106 |
107 | @doc ~S"""
108 | Removes all instances of the given header.
109 |
110 | Header names are matched case-insensitively.
111 |
112 | Example:
113 |
114 | iex> headers = [
115 | ...> {"header1", "foo"},
116 | ...> {"header2", "bar"},
117 | ...> {"Header1", "baz"}
118 | ...> ]
119 | iex> Mojito.Headers.delete(headers, "HEADER1")
120 | [{"header2", "bar"}]
121 | """
122 | @spec delete(headers, String.t()) :: headers
123 | def delete(headers, name) do
124 | name = String.downcase(name)
125 | Enum.filter(headers, fn {key, _value} -> String.downcase(key) != name end)
126 | end
127 |
128 | @doc ~S"""
129 | Returns an ordered list of the header names from the given headers.
130 | Header names are returned in lowercase.
131 |
132 | Example:
133 |
134 | iex> headers = [
135 | ...> {"header1", "foo"},
136 | ...> {"header2", "bar"},
137 | ...> {"Header1", "baz"}
138 | ...> ]
139 | iex> Mojito.Headers.keys(headers)
140 | ["header1", "header2"]
141 | """
142 | @spec keys(headers) :: [String.t()]
143 | def keys(headers) do
144 | keys(headers, [])
145 | end
146 |
147 | defp keys([], names), do: Enum.reverse(names)
148 |
149 | defp keys([{name, _value} | rest], names) do
150 | name = String.downcase(name)
151 |
152 | if name in names do
153 | keys(rest, names)
154 | else
155 | keys(rest, [name | names])
156 | end
157 | end
158 |
159 | @doc ~S"""
160 | Returns a copy of the given headers where all header names are lowercased
161 | and multiple values for the same header have been joined with `","`.
162 |
163 | Example:
164 |
165 | iex> headers = [
166 | ...> {"header1", "foo"},
167 | ...> {"header2", "bar"},
168 | ...> {"Header1", "baz"}
169 | ...> ]
170 | iex> Mojito.Headers.normalize(headers)
171 | [{"header1", "foo,baz"}, {"header2", "bar"}]
172 | """
173 | @spec normalize(headers) :: headers
174 | def normalize(headers) do
175 | headers_map =
176 | Enum.reduce(headers, %{}, fn {name, value}, acc ->
177 | name = String.downcase(name)
178 | values = Map.get(acc, name, [])
179 | Map.put(acc, name, values ++ [value])
180 | end)
181 |
182 | headers
183 | |> keys
184 | |> Enum.map(fn name ->
185 | {name, Map.get(headers_map, name) |> Enum.join(",")}
186 | end)
187 | end
188 |
189 | @doc ~S"""
190 | Returns an HTTP Basic Auth header from the given username and password.
191 |
192 | Example:
193 |
194 | iex> Mojito.Headers.auth_header("hello", "world")
195 | {"authorization", "Basic aGVsbG86d29ybGQ="}
196 | """
197 | @spec auth_header(String.t(), String.t()) :: Mojito.header()
198 | def auth_header(username, password) do
199 | auth64 = "#{username}:#{password}" |> Base.encode64()
200 | {"authorization", "Basic #{auth64}"}
201 | end
202 |
203 | @doc ~S"""
204 | Convert non string values to string where is possible.
205 |
206 | Example:
207 |
208 | iex> Mojito.Headers.convert_values_to_string([{"content-length", 0}])
209 | [{"content-length", "0"}]
210 | """
211 | @spec convert_values_to_string(headers) :: headers
212 | def convert_values_to_string(headers) do
213 | convert_values_to_string(headers, [])
214 | end
215 |
216 | defp convert_values_to_string([], converted_headers),
217 | do: Enum.reverse(converted_headers)
218 |
219 | defp convert_values_to_string([{name, value} | rest], converted_headers)
220 | when is_number(value) or is_atom(value) do
221 | convert_values_to_string(rest, [
222 | {name, to_string(value)} | converted_headers
223 | ])
224 | end
225 |
226 | defp convert_values_to_string([headers | rest], converted_headers) do
227 | convert_values_to_string(rest, [headers | converted_headers])
228 | end
229 | end
230 |
--------------------------------------------------------------------------------
/lib/mojito/pool.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Pool do
2 | @moduledoc false
3 |
4 | @callback request(request :: Mojito.request()) ::
5 | {:ok, Mojito.response()} | {:error, Mojito.error()}
6 |
7 | @type pool_opts :: [pool_opt | {:destinations, [pool_opt]}]
8 |
9 | @type pool_opt ::
10 | {:size, pos_integer}
11 | | {:max_overflow, non_neg_integer}
12 | | {:pools, pos_integer}
13 | | {:strategy, :lifo | :fifo}
14 |
15 | @type pool_key :: {String.t(), pos_integer}
16 |
17 | @default_pool_opts [
18 | size: 5,
19 | max_overflow: 10,
20 | pools: 5,
21 | strategy: :lifo
22 | ]
23 |
24 | ## Returns the configured `t:pool_opts` for the given destination.
25 | @doc false
26 | @spec pool_opts(pool_key) :: Mojito.pool_opts()
27 | def pool_opts({host, port}) do
28 | destination_key =
29 | try do
30 | "#{host}:#{port}" |> String.to_existing_atom()
31 | rescue
32 | _ -> :none
33 | end
34 |
35 | config_pool_opts = Application.get_env(:mojito, :pool_opts, [])
36 |
37 | destination_pool_opts =
38 | config_pool_opts
39 | |> Keyword.get(:destinations, [])
40 | |> Keyword.get(destination_key, [])
41 |
42 | @default_pool_opts
43 | |> Keyword.merge(config_pool_opts)
44 | |> Keyword.merge(destination_pool_opts)
45 | |> Keyword.delete(:destinations)
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/mojito/pool/poolboy.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Pool.Poolboy do
2 | @moduledoc false
3 |
4 | ## Mojito.Pool.Poolboy is an HTTP client with high-performance, easy-to-use
5 | ## connection pools based on the Poolboy library.
6 | ##
7 | ## Pools are maintained automatically by Mojito, requests are matched to
8 | ## the correct pool without user intervention, and multiple pools can be
9 | ## used for the same destination in order to reduce concurrency bottlenecks.
10 | ##
11 | ## Config parameters are explained in the `Mojito` moduledocs.
12 |
13 | @behaviour Mojito.Pool
14 |
15 | alias Mojito.{Config, Request, Utils}
16 | require Logger
17 |
18 | @doc ~S"""
19 | Performs an HTTP request using a connection pool, creating that pool if
20 | it didn't already exist. Requests are always matched to a pool that is
21 | connected to the correct destination host and port.
22 | """
23 | @impl true
24 | def request(%{} = request) do
25 | with {:ok, valid_request} <- Request.validate_request(request),
26 | {:ok, _proto, host, port} <- Utils.decompose_url(valid_request.url),
27 | pool_key <- pool_key(host, port),
28 | {:ok, pool} <- get_pool(pool_key) do
29 | do_request(pool, pool_key, valid_request)
30 | end
31 | end
32 |
33 | defp do_request(pool, pool_key, request) do
34 | case Mojito.Pool.Poolboy.Single.request(pool, request) do
35 | {:error, %{reason: :checkout_timeout}} ->
36 | case start_pool(pool_key) do
37 | {:ok, pid} ->
38 | Mojito.Pool.Poolboy.Single.request(pid, request)
39 |
40 | error ->
41 | error
42 | end
43 |
44 | other ->
45 | other
46 | end
47 | end
48 |
49 | ## Returns a pool for the given destination, starting one or more
50 | ## if necessary.
51 | @doc false
52 | @spec get_pool(any) :: {:ok, pid} | {:error, Mojito.error()}
53 | def get_pool(pool_key) do
54 | case get_pools(pool_key) do
55 | [] ->
56 | opts = Mojito.Pool.pool_opts(pool_key)
57 | 1..opts[:pools] |> Enum.each(fn _ -> start_pool(pool_key) end)
58 | get_pool(pool_key)
59 |
60 | pools ->
61 | {:ok, Enum.random(pools)}
62 | end
63 | end
64 |
65 | ## Returns all pools for the given destination.
66 | @doc false
67 | @spec get_pools(any) :: [pid]
68 | defp get_pools(pool_key) do
69 | Mojito.Pool.Poolboy.Registry
70 | |> Registry.lookup(pool_key)
71 | |> Enum.map(fn {_, pid} -> pid end)
72 | end
73 |
74 | ## Starts a new pool for the given destination.
75 | @doc false
76 | @spec start_pool(any) :: {:ok, pid} | {:error, Mojito.error()}
77 | def start_pool(pool_key) do
78 | old_trap_exit = Process.flag(:trap_exit, true)
79 |
80 | try do
81 | GenServer.call(
82 | Mojito.Pool.Poolboy.Manager,
83 | {:start_pool, pool_key},
84 | Config.timeout()
85 | )
86 | rescue
87 | e -> {:error, e}
88 | catch
89 | :exit, _ -> {:error, :checkout_timeout}
90 | after
91 | Process.flag(:trap_exit, old_trap_exit)
92 | end
93 | |> Utils.wrap_return_value()
94 | end
95 |
96 | ## Returns a key representing the given destination.
97 | @doc false
98 | @spec pool_key(String.t(), pos_integer) :: Mojito.Pool.pool_key()
99 | def pool_key(host, port) do
100 | {host, port}
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/lib/mojito/pool/poolboy/manager.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Pool.Poolboy.Manager do
2 | ## I'd prefer to start new pools directly in the caller process, but
3 | ## they'd end up disappearing from the registry when the process
4 | ## terminates. So instead we start new pools from here, a long-lived
5 | ## GenServer, and link them to Mojito.Supervisor instead of to here.
6 |
7 | @moduledoc false
8 |
9 | use GenServer
10 | alias Mojito.Telemetry
11 |
12 | def start_link(args) do
13 | GenServer.start_link(__MODULE__, args, name: __MODULE__)
14 | end
15 |
16 | def init(args) do
17 | {:ok, %{args: args, pools: %{}, last_start_at: %{}}}
18 | end
19 |
20 | defp time, do: System.monotonic_time(:millisecond)
21 |
22 | def handle_call({:start_pool, pool_key}, _from, state) do
23 | pool_opts = Mojito.Pool.pool_opts(pool_key)
24 | max_pools = pool_opts[:pools]
25 |
26 | pools = state.pools |> Map.get(pool_key, [])
27 | npools = Enum.count(pools)
28 |
29 | cond do
30 | npools >= max_pools ->
31 | ## We're at max, don't start a new pool
32 | {:reply, {:ok, Enum.random(pools)}, state}
33 |
34 | :else ->
35 | actually_start_pool(pool_key, pool_opts, pools, npools, state)
36 | end
37 | end
38 |
39 | def handle_call(:get_all_pool_states, _from, state) do
40 | all_pool_states =
41 | state.pools
42 | |> Enum.map(fn {pool_key, pools} ->
43 | {pool_key, pools |> Enum.map(&get_poolboy_state/1)}
44 | end)
45 | |> Enum.into(%{})
46 |
47 | {:reply, all_pool_states, state}
48 | end
49 |
50 | def handle_call({:get_pool_states, pool_key}, _from, state) do
51 | pools = state.pools |> Map.get(pool_key, [])
52 | pool_states = pools |> Enum.map(&get_poolboy_state/1)
53 | {:reply, pool_states, state}
54 | end
55 |
56 | def handle_call({:get_pools, pool_key}, _from, state) do
57 | {:reply, Map.get(state.pools, pool_key, []), state}
58 | end
59 |
60 | def handle_call(:state, _from, state) do
61 | {:reply, state, state}
62 | end
63 |
64 | defp get_poolboy_state(pool_pid) do
65 | {:state, supervisor, workers, waiting, monitors, size, overflow,
66 | max_overflow, strategy} = :sys.get_state(pool_pid)
67 |
68 | %{
69 | supervisor: supervisor,
70 | workers: workers,
71 | waiting: waiting,
72 | monitors: monitors,
73 | size: size,
74 | overflow: overflow,
75 | max_overflow: max_overflow,
76 | strategy: strategy
77 | }
78 | end
79 |
80 | ## This is designed to be able to launch pools on-demand, but for now we
81 | ## launch all pools at once in Mojito.Pool.
82 | defp actually_start_pool(pool_key, pool_opts, pools, npools, state) do
83 | {host, port} = pool_key
84 | meta = %{host: host, port: port}
85 | start = Telemetry.start(:pool, meta)
86 |
87 | pool_id = {Mojito.Pool, pool_key, npools}
88 |
89 | child_spec =
90 | pool_opts
91 | |> Keyword.put(:id, pool_id)
92 | |> Mojito.Pool.Poolboy.Single.child_spec()
93 |
94 | with {:ok, pool_pid} <-
95 | Supervisor.start_child(Mojito.Supervisor, child_spec),
96 | {:ok, _} <-
97 | Registry.register(Mojito.Pool.Poolboy.Registry, pool_key, pool_pid) do
98 | state =
99 | state
100 | |> put_in([:pools, pool_key], [pool_pid | pools])
101 | |> put_in([:last_start_at, pool_key], time())
102 |
103 | Telemetry.stop(:pool, start, meta)
104 |
105 | {:reply, {:ok, pool_pid}, state}
106 | else
107 | {:error, {msg, _pid}}
108 | when msg in [:already_started, :already_registered] ->
109 | ## There was a race; we lost and that is fine
110 | Telemetry.stop(:pool, start, meta)
111 | {:reply, {:ok, Enum.random(pools)}, state}
112 |
113 | error ->
114 | Telemetry.stop(:pool, start, meta)
115 | {:reply, error, state}
116 | end
117 | end
118 | end
119 |
--------------------------------------------------------------------------------
/lib/mojito/pool/poolboy/single.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Pool.Poolboy.Single do
2 | @moduledoc false
3 |
4 | ## Mojito.Pool.Poolboy.Single provides an HTTP request connection pool based on
5 | ## Mojito and Poolboy.
6 | ##
7 | ## Example:
8 | ##
9 | ## >>>> child_spec = Mojito.Pool.Poolboy.Single.child_spec()
10 | ## >>>> {:ok, pool_pid} = Supervisor.start_child(Mojito.Supervisor, child_spec)
11 | ## >>>> Mojito.Pool.Poolboy.Single.request(pool_pid, :get, "http://example.com")
12 | ## {:ok, %Mojito.Response{...}}
13 |
14 | alias Mojito.{Config, ConnServer, Request, Telemetry, Utils}
15 |
16 | @doc false
17 | @deprecated "Use child_spec/1 instead"
18 | def child_spec(name, opts) do
19 | opts
20 | |> Keyword.put(:name, name)
21 | |> child_spec
22 | end
23 |
24 | @doc ~S"""
25 | Returns a child spec suitable to pass to e.g., `Supervisor.start_link/2`.
26 |
27 | Options:
28 |
29 | * `:name` sets a global name for the pool. Optional.
30 | * `:size` sets the initial pool size. Default is 10.
31 | * `:max_overflow` sets the maximum number of additional connections
32 | under high load. Default is 5.
33 | * `:strategy` sets the pool connection-grabbing strategy. Valid values
34 | are `:fifo` and `:lifo` (default).
35 | """
36 | def child_spec(opts \\ [])
37 |
38 | def child_spec(name) when is_binary(name) do
39 | child_spec(name: name)
40 | end
41 |
42 | def child_spec(opts) do
43 | name = opts[:name]
44 |
45 | name_opts =
46 | case name do
47 | nil -> []
48 | name -> [name: name]
49 | end
50 |
51 | poolboy_opts = [{:worker_module, Mojito.ConnServer} | opts]
52 |
53 | poolboy_opts =
54 | case name do
55 | nil -> poolboy_opts
56 | name -> [{:name, {:local, name}} | poolboy_opts]
57 | end
58 |
59 | %{
60 | id: opts[:id] || {Mojito.Pool, make_ref()},
61 | start: {:poolboy, :start_link, [poolboy_opts, name_opts]},
62 | restart: :permanent,
63 | shutdown: 5000,
64 | type: :worker
65 | }
66 | end
67 |
68 | @doc ~S"""
69 | Makes an HTTP request using the given connection pool.
70 |
71 | See `request/2` for documentation.
72 | """
73 | @spec request(
74 | pid,
75 | Mojito.method(),
76 | String.t(),
77 | Mojito.headers(),
78 | String.t(),
79 | Keyword.t()
80 | ) :: {:ok, Mojito.response()} | {:error, Mojito.error()}
81 | def request(pool, method, url, headers \\ [], body \\ "", opts \\ []) do
82 | req = %Request{
83 | method: method,
84 | url: url,
85 | headers: headers,
86 | body: body,
87 | opts: opts
88 | }
89 |
90 | request(pool, req)
91 | end
92 |
93 | @doc ~S"""
94 | Makes an HTTP request using the given connection pool.
95 |
96 | Options:
97 |
98 | * `:timeout` - Request timeout in milliseconds. Defaults to
99 | `Application.get_env(:mojito, :timeout, 5000)`.
100 | * `:max_body_size` - Max body size in bytes. Defaults to nil in which
101 | case no max size will be enforced.
102 | * `:transport_opts` - Options to be passed to either `:gen_tcp` or `:ssl`.
103 | Most commonly used to perform insecure HTTPS requests via
104 | `transport_opts: [verify: :verify_none]`.
105 | """
106 | @spec request(pid, Mojito.request()) ::
107 | {:ok, Mojito.response()} | {:error, Mojito.error()}
108 | def request(pool, request) do
109 | ## TODO refactor so request.url is already a URI struct when it gets here
110 | uri = URI.parse(request.url)
111 |
112 | meta = %{
113 | host: uri.host,
114 | port: uri.port,
115 | path: uri.path,
116 | method: request.method
117 | }
118 |
119 | start_time = Telemetry.start(:request, meta)
120 |
121 | with {:ok, valid_request} <- Request.validate_request(request) do
122 | timeout = valid_request.opts[:timeout] || Config.timeout()
123 |
124 | case do_request(pool, valid_request) do
125 | {:error, %Mojito.Error{reason: %{reason: :closed}}} ->
126 | ## Retry connection-closed errors as many times as we can
127 | new_timeout = calc_new_timeout(timeout, start_time)
128 |
129 | new_request_opts =
130 | valid_request.opts |> Keyword.put(:timeout, new_timeout)
131 |
132 | request(pool, %{valid_request | opts: new_request_opts})
133 |
134 | other ->
135 | Telemetry.stop(:request, start_time, meta)
136 | other
137 | end
138 | end
139 | end
140 |
141 | defp do_request(pool, request) do
142 | timeout = request.opts[:timeout] || Config.timeout()
143 | start_time = time()
144 | response_ref = make_ref()
145 |
146 | worker_fn = fn worker ->
147 | case ConnServer.request(worker, request, self(), response_ref) do
148 | :ok ->
149 | new_timeout = calc_new_timeout(timeout, start_time) |> max(0)
150 |
151 | receive do
152 | {:mojito_response, ^response_ref, response} ->
153 | ## reduce memory footprint of idle pool
154 | :erlang.garbage_collect(worker)
155 |
156 | response
157 | after
158 | new_timeout -> {:error, :timeout}
159 | end
160 |
161 | e ->
162 | e
163 | end
164 | end
165 |
166 | old_trap_exit = Process.flag(:trap_exit, true)
167 |
168 | try do
169 | :poolboy.transaction(pool, worker_fn, timeout)
170 | rescue
171 | e -> {:error, e}
172 | catch
173 | :exit, _ -> {:error, :checkout_timeout}
174 | after
175 | Process.flag(:trap_exit, old_trap_exit)
176 | end
177 | |> Utils.wrap_return_value()
178 | end
179 |
180 | defp calc_new_timeout(:infinity, _), do: :infinity
181 |
182 | defp calc_new_timeout(timeout, start_time) do
183 | timeout - (time() - start_time)
184 | end
185 |
186 | defp time, do: System.monotonic_time(:millisecond)
187 | end
188 |
--------------------------------------------------------------------------------
/lib/mojito/request.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Request do
2 | @moduledoc false
3 |
4 | defstruct method: nil,
5 | url: nil,
6 | headers: [],
7 | body: "",
8 | opts: []
9 |
10 | alias Mojito.{Error, Headers, Request}
11 |
12 | @doc ~S"""
13 | Checks for errors and returns a canonicalized version of the request.
14 | """
15 | @spec validate_request(map | Mojito.request() | Mojito.request_kwlist()) ::
16 | {:ok, Mojito.request()} | {:error, Mojito.error()}
17 |
18 | def validate_request(%{} = request) do
19 | method = Map.get(request, :method)
20 | url = Map.get(request, :url)
21 | headers = Map.get(request, :headers, [])
22 | body = Map.get(request, :body)
23 | opts = Map.get(request, :opts, [])
24 |
25 | cond do
26 | method == nil ->
27 | {:error, %Error{message: "method cannot be nil"}}
28 |
29 | method == "" ->
30 | {:error, %Error{message: "method cannot be blank"}}
31 |
32 | url == nil ->
33 | {:error, %Error{message: "url cannot be nil"}}
34 |
35 | url == "" ->
36 | {:error, %Error{message: "url cannot be blank"}}
37 |
38 | !is_list(headers) ->
39 | {:error, %Error{message: "headers must be a list"}}
40 |
41 | !is_binary(body) && !is_nil(body) ->
42 | {:error, %Error{message: "body must be `nil` or a UTF-8 string"}}
43 |
44 | :else ->
45 | method_atom = method_to_atom(method)
46 |
47 | ## Prevent bug #58, where sending "" with HEAD/GET/OPTIONS
48 | ## can screw up HTTP/2 handling
49 | valid_body =
50 | case method_atom do
51 | :get -> nil
52 | :head -> nil
53 | :delete -> nil
54 | :options -> nil
55 | _ -> request.body || ""
56 | end
57 |
58 | {:ok,
59 | %Request{
60 | method: method_atom,
61 | url: url,
62 | headers: headers,
63 | body: valid_body,
64 | opts: opts
65 | }}
66 | end
67 | end
68 |
69 | def validate_request(request) when is_list(request) do
70 | request |> Enum.into(%{}) |> validate_request
71 | end
72 |
73 | def validate_request(_request) do
74 | {:error, %Error{message: "request must be a map"}}
75 | end
76 |
77 | defp method_to_atom(method) when is_atom(method), do: method
78 |
79 | defp method_to_atom(method) when is_binary(method) do
80 | method |> String.downcase() |> String.to_atom()
81 | end
82 |
83 | @doc ~S"""
84 | Converts non-string header values to UTF-8 string if possible.
85 | """
86 | @spec convert_headers_values_to_string(Mojito.request()) ::
87 | {:ok, Mojito.request()}
88 | def convert_headers_values_to_string(%{headers: headers} = request) do
89 | {:ok, %{request | headers: Headers.convert_values_to_string(headers)}}
90 | end
91 | end
92 |
--------------------------------------------------------------------------------
/lib/mojito/request/single.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Request.Single do
2 | ## Make a single request, without spawning any processes.
3 |
4 | @moduledoc false
5 |
6 | alias Mojito.{Config, Conn, Error, Request, Response}
7 | require Logger
8 |
9 | @doc ~S"""
10 | Performs a single HTTP request, receiving `:tcp` and `:ssl` messages
11 | in the caller process.
12 |
13 | Options:
14 |
15 | * `:timeout` - Response timeout in milliseconds. Defaults to
16 | `Application.get_env(:mojito, :timeout, 5000)`.
17 | * `:max_body_size` - Max body size in bytes. Defaults to nil in which
18 | case no max size will be enforced.
19 | * `:transport_opts` - Options to be passed to either `:gen_tcp` or `:ssl`.
20 | Most commonly used to perform insecure HTTPS requests via
21 | `transport_opts: [verify: :verify_none]`.
22 | """
23 | @spec request(Mojito.request()) ::
24 | {:ok, Mojito.response()} | {:error, Mojito.error()}
25 | def request(%Request{} = req) do
26 | with_connection(req, fn conn ->
27 | with {:ok, conn, _ref, response} <- Conn.request(conn, req) do
28 | timeout = req.opts[:timeout] || Config.timeout()
29 | receive_response(conn, response, timeout)
30 | end
31 | end)
32 | end
33 |
34 | defp time, do: System.monotonic_time(:millisecond)
35 |
36 | @doc false
37 | def receive_response(conn, response, timeout) do
38 | start_time = time()
39 |
40 | receive do
41 | {:tcp, _, _} = msg ->
42 | handle_msg(conn, response, timeout, msg, start_time)
43 |
44 | {:tcp_closed, _} = msg ->
45 | handle_msg(conn, response, timeout, msg, start_time)
46 |
47 | {:ssl, _, _} = msg ->
48 | handle_msg(conn, response, timeout, msg, start_time)
49 |
50 | {:ssl_closed, _} = msg ->
51 | handle_msg(conn, response, timeout, msg, start_time)
52 | after
53 | timeout -> {:error, %Error{reason: :timeout}}
54 | end
55 | end
56 |
57 | defp handle_msg(conn, response, timeout, msg, start_time) do
58 | new_timeout = fn ->
59 | case timeout do
60 | :infinity ->
61 | :infinity
62 |
63 | _ ->
64 | time_elapsed = time() - start_time
65 |
66 | case timeout - time_elapsed do
67 | x when x < 0 -> 0
68 | x -> x
69 | end
70 | end
71 | end
72 |
73 | case Mint.HTTP.stream(conn.conn, msg) do
74 | {:ok, mint_conn, resps} ->
75 | conn = %{conn | conn: mint_conn}
76 |
77 | case Response.apply_resps(response, resps) do
78 | {:ok, %{complete: true} = response} -> {:ok, response}
79 | {:ok, response} -> receive_response(conn, response, new_timeout.())
80 | err -> err
81 | end
82 |
83 | {:error, _, e, _} ->
84 | {:error, %Error{reason: e}}
85 |
86 | :unknown ->
87 | receive_response(conn, response, new_timeout.())
88 | end
89 | end
90 |
91 | defp with_connection(req, fun) do
92 | with {:ok, req} <- Request.validate_request(req),
93 | {:ok, conn} <- Conn.connect(req.url, req.opts) do
94 | try do
95 | fun.(conn)
96 | after
97 | Conn.close(conn)
98 | end
99 | end
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/lib/mojito/response.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Response do
2 | @moduledoc false
3 |
4 | alias Mojito.{Error, Response}
5 |
6 | defstruct status_code: nil,
7 | headers: [],
8 | body: "",
9 | complete: false,
10 | size: 0
11 |
12 | @type t :: Mojito.response()
13 |
14 | @doc ~S"""
15 | Applies responses received from `Mint.HTTP.stream/2` to a `%Mojito.Response{}`.
16 | """
17 | @spec apply_resps(t, [Mint.Types.response()]) :: {:ok, t} | {:error, any}
18 | def apply_resps(response, []), do: {:ok, response}
19 |
20 | def apply_resps(response, [mint_resp | rest]) do
21 | with {:ok, response} <- apply_resp(response, mint_resp) do
22 | apply_resps(response, rest)
23 | end
24 | end
25 |
26 | @doc ~S"""
27 | Applies a response received from `Mint.HTTP.stream/2` to a `%Mojito.Response{}`.
28 | """
29 | @spec apply_resps(t, Mint.Types.response()) :: {:ok, t} | {:error, any}
30 | def apply_resp(response, {:status, _request_ref, status_code}) do
31 | {:ok, %{response | status_code: status_code}}
32 | end
33 |
34 | def apply_resp(response, {:headers, _request_ref, headers}) do
35 | {:ok, %{response | headers: headers}}
36 | end
37 |
38 | def apply_resp(response, {:data, _request_ref, chunk}) do
39 | with {:ok, response} <- put_chunk(response, chunk) do
40 | {:ok, response}
41 | end
42 | end
43 |
44 | def apply_resp(response, {:done, _request_ref}) do
45 | body = :erlang.iolist_to_binary(response.body)
46 | size = byte_size(body)
47 | {:ok, %{response | complete: true, body: body, size: size}}
48 | end
49 |
50 | @doc ~S"""
51 | Adds chunks to a response body, respecting the `response.size` field.
52 | `response.size` should be set to the maximum number of bytes to accept
53 | as the response body, or `nil` for no limit.
54 | """
55 | @spec put_chunk(t, binary) :: {:ok, %Response{}} | {:error, any}
56 | def put_chunk(%Response{size: nil} = response, chunk) do
57 | {:ok, %{response | body: [response.body | [chunk]]}}
58 | end
59 |
60 | def put_chunk(%Response{size: remaining} = response, chunk) do
61 | case remaining - byte_size(chunk) do
62 | over_limit when over_limit < 0 ->
63 | {:error, %Error{reason: :max_body_size_exceeded}}
64 |
65 | new_remaining ->
66 | {:ok,
67 | %{response | body: [response.body | [chunk]], size: new_remaining}}
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/lib/mojito/telemetry.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Telemetry do
2 | @moduledoc ~S"""
3 | Mojito's [Telemetry](https://github.com/beam-telemetry/telemetry)
4 | integration.
5 |
6 | All time measurements are emitted in `:millisecond` units by
7 | default. A different
8 | [Erlang time unit](https://erlang.org/doc/man/erlang.html#type-time_unit)
9 | can be chosen by setting a config parameter like so:
10 |
11 | ```
12 | config :mojito, Mojito.Telemetry, time_unit: :microsecond
13 | ```
14 |
15 | Mojito emits the following Telemetry events:
16 |
17 | * `[:mojito, :pool, :start]` before launching a pool
18 | - Measurements: `:system_time`
19 | - Metadata: `:host`, `:port`
20 |
21 | * `[:mojito, :pool, :stop]` after launching a pool
22 | - Measurements: `:system_time`, `:duration`
23 | - Metadata: `:host`, `:port`
24 |
25 | * `[:mojito, :connect, :start]` before connecting to a host
26 | - Measurements: `:system_time`
27 | - Metadata: `:host`, `:port`
28 |
29 | * `[:mojito, :connect, :stop]` after connecting to a host
30 | - Measurements: `:system_time`, `:duration`
31 | - Metadata: `:host`, `:port`
32 |
33 | * `[:mojito, :request, :start]` before making a request
34 | - Measurements: `:system_time`
35 | - Metadata: `:host`, `:port`, `:path`, `:method`
36 |
37 | * `[:mojito, :request, :stop]` after making a request
38 | - Measurements: `:system_time`, `:duration`
39 | - Metadata: `:host`, `:port`, `:path`, `:method`
40 |
41 | """
42 |
43 | @typep monotonic_time :: integer
44 |
45 | defp time_unit do
46 | Application.get_env(:mojito, Mojito.Telemetry)[:time_unit] || :millisecond
47 | end
48 |
49 | defp monotonic_time do
50 | :erlang.monotonic_time(time_unit())
51 | end
52 |
53 | defp system_time do
54 | :erlang.system_time(time_unit())
55 | end
56 |
57 | @doc false
58 | @spec start(atom, map) :: monotonic_time
59 | def start(name, meta \\ %{}) do
60 | start_time = monotonic_time()
61 |
62 | :telemetry.execute(
63 | [:mojito, name, :start],
64 | %{system_time: system_time()},
65 | meta
66 | )
67 |
68 | start_time
69 | end
70 |
71 | @doc false
72 | @spec stop(atom, monotonic_time, map) :: monotonic_time
73 | def stop(name, start_time, meta \\ %{}) do
74 | stop_time = monotonic_time()
75 | duration = stop_time - start_time
76 |
77 | :telemetry.execute(
78 | [:mojito, name, :stop],
79 | %{system_time: system_time(), duration: duration},
80 | meta
81 | )
82 |
83 | stop_time
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/lib/mojito/utils.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Utils do
2 | @moduledoc false
3 |
4 | alias Mojito.Error
5 |
6 | @doc ~S"""
7 | Ensures that the return value errors are of the form
8 | `{:error, %Mojito.Error{}}`. Values `:ok` and `{:ok, val}` are
9 | considered successful; other values are treated as errors.
10 | """
11 | @spec wrap_return_value(any) :: :ok | {:ok, any} | {:error, Mojito.error()}
12 | def wrap_return_value(rv) do
13 | case rv do
14 | :ok -> rv
15 | {:ok, _} -> rv
16 | {:error, %Error{}} -> rv
17 | {:error, {:error, e}} -> {:error, %Error{reason: e}}
18 | {:error, e} -> {:error, %Error{reason: e}}
19 | {:error, _mint_conn, error} -> {:error, %Error{reason: error}}
20 | other -> {:error, %Error{reason: :unknown, message: other}}
21 | end
22 | end
23 |
24 | @doc ~S"""
25 | Returns the protocol, hostname, and port (express or implied) from a
26 | web URL.
27 |
28 | iex> Mojito.Utils.decompose_url("http://example.com:8888/test")
29 | {:ok, "http", "example.com", 8888}
30 |
31 | iex> Mojito.Utils.decompose_url("https://user:pass@example.com")
32 | {:ok, "https", "example.com", 443}
33 | """
34 | @spec decompose_url(String.t()) ::
35 | {:ok, String.t(), String.t(), non_neg_integer} | {:error, any}
36 | def decompose_url(url) do
37 | try do
38 | uri = URI.parse(url)
39 |
40 | cond do
41 | !uri.scheme || !uri.host || !uri.port ->
42 | {:error, %Error{message: "invalid URL: #{url}"}}
43 |
44 | :else ->
45 | {:ok, uri.scheme, uri.host, uri.port}
46 | end
47 | rescue
48 | e -> {:error, %Error{message: "invalid URL", reason: e}}
49 | end
50 | end
51 |
52 | @doc ~S"""
53 | Returns a relative URL including query parts, excluding the fragment, and any
54 | necessary auth headers (i.e., for HTTP Basic auth).
55 |
56 | iex> Mojito.Utils.get_relative_url_and_auth_headers("https://user:pass@example.com/this/is/awesome?foo=bar&baz")
57 | {:ok, "/this/is/awesome?foo=bar&baz", [{"authorization", "Basic dXNlcjpwYXNz"}]}
58 |
59 | iex> Mojito.Utils.get_relative_url_and_auth_headers("https://example.com/something.html#section42")
60 | {:ok, "/something.html", []}
61 | """
62 | @spec get_relative_url_and_auth_headers(String.t()) ::
63 | {:ok, String.t(), Mojito.headers()} | {:error, any}
64 | def get_relative_url_and_auth_headers(url) do
65 | try do
66 | uri = URI.parse(url)
67 |
68 | headers =
69 | case uri.userinfo do
70 | nil -> []
71 | userinfo -> [{"authorization", "Basic #{Base.encode64(userinfo)}"}]
72 | end
73 |
74 | joined_url =
75 | [
76 | if(uri.path, do: "#{uri.path}", else: ""),
77 | if(uri.query, do: "?#{uri.query}", else: "")
78 | ]
79 | |> Enum.join("")
80 |
81 | relative_url =
82 | if String.starts_with?(joined_url, "/") do
83 | joined_url
84 | else
85 | "/" <> joined_url
86 | end
87 |
88 | {:ok, relative_url, headers}
89 | rescue
90 | e -> {:error, %Error{message: "invalid URL", reason: e}}
91 | end
92 | end
93 |
94 | @doc ~S"""
95 | Returns the correct Erlang TCP transport module for the given protocol.
96 | """
97 | @spec protocol_to_transport(String.t()) :: {:ok, atom} | {:error, any}
98 | def protocol_to_transport("https"), do: {:ok, :ssl}
99 |
100 | def protocol_to_transport("http"), do: {:ok, :gen_tcp}
101 |
102 | def protocol_to_transport(proto),
103 | do: {:error, "unknown protocol #{inspect(proto)}"}
104 | end
105 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Mojito.MixProject do
2 | use Mix.Project
3 |
4 | @version "0.7.12"
5 | @repo_url "https://github.com/appcues/mojito"
6 |
7 | def project do
8 | [
9 | app: :mojito,
10 | version: @version,
11 | elixir: "~> 1.7",
12 | elixirc_paths: elixirc_paths(Mix.env()),
13 | start_permanent: Mix.env() == :prod,
14 | dialyzer: [
15 | plt_add_apps: [:mix]
16 | ],
17 | deps: deps(),
18 | package: package(),
19 | docs: docs()
20 | ]
21 | end
22 |
23 | defp elixirc_paths(:test), do: ["lib", "test/support"]
24 | defp elixirc_paths(_), do: ["lib"]
25 |
26 | defp package do
27 | [
28 | description: "Fast, easy to use HTTP client based on Mint",
29 | licenses: ["MIT"],
30 | maintainers: ["pete gamache "],
31 | links: %{
32 | Changelog: "https://hexdocs.pm/mojito/changelog.html",
33 | GitHub: @repo_url
34 | }
35 | ]
36 | end
37 |
38 | def application do
39 | [
40 | extra_applications: [:logger],
41 | mod: {Mojito.Application, []}
42 | ]
43 | end
44 |
45 | defp deps do
46 | [
47 | {:mint, "~> 1.1"},
48 | {:castore, "~> 0.1"},
49 | {:poolboy, "~> 1.5"},
50 | {:telemetry, "~> 0.4 or ~> 1.0"},
51 | {:ex_spec, "~> 2.0", only: :test},
52 | {:jason, "~> 1.0", only: :test},
53 | {:cowboy, "~> 2.0", only: :test},
54 | {:plug, "~> 1.3", only: :test},
55 | {:plug_cowboy, "~> 2.0", only: :test},
56 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
57 | {:dialyxir, "~> 1.0", only: :dev, runtime: false}
58 | ]
59 | end
60 |
61 | defp docs do
62 | [
63 | extras: [
64 | "CHANGELOG.md": [title: "Changelog"],
65 | "LICENSE.md": [title: "License"]
66 | ],
67 | assets: "assets",
68 | logo: "assets/mojito.png",
69 | main: "Mojito",
70 | source_url: @repo_url,
71 | source_ref: @version,
72 | formatters: ["html"]
73 | ]
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "castore": {:hex, :castore, "0.1.14", "3f6d7c7c1574c402fef29559d3f1a7389ba3524bc6a090a5e9e6abc3af65dcca", [:mix], [], "hexpm", "b34af542eadb727e6c8b37fdf73e18b2e02eb483a4ea0b52fd500bc23f052b7b"},
3 | "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
4 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
5 | "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
6 | "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
7 | "earmark": {:hex, :earmark, "1.4.9", "837e4c1c5302b3135e9955f2bbf52c6c52e950c383983942b68b03909356c0d9", [:mix], [{:earmark_parser, ">= 1.4.9", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "0d72df7d13a3dc8422882bed5263fdec5a773f56f7baeb02379361cb9e5b0d8e"},
8 | "earmark_parser": {:hex, :earmark_parser, "1.4.18", "e1b2be73eb08a49fb032a0208bf647380682374a725dfb5b9e510def8397f6f2", [:mix], [], "hexpm", "114a0e85ec3cf9e04b811009e73c206394ffecfcc313e0b346de0d557774ee97"},
9 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
10 | "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"},
11 | "ex_spec": {:hex, :ex_spec, "2.0.1", "8bdbd6fa85995fbf836ed799571d44be6f9ebbcace075209fd0ad06372c111cf", [:mix], [], "hexpm", "b44fe5054497411a58341ece5bf7756c219d9d6c1303b5ac467f557a0a4c31ac"},
12 | "freedom_formatter": {:hex, :freedom_formatter, "1.0.0", "b19be4a845082d05d32bb23765e8e7bdc6d51decac13ab64ae44b0f6bf8a66d1", [:mix], [], "hexpm"},
13 | "fuzzyurl": {:hex, :fuzzyurl, "1.0.1", "389780519adccfc3582ecce8c6608c1619f029367abfdc9d4b6e46491d2fa8d5", [:mix], [], "hexpm"},
14 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
15 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
16 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"},
17 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
18 | "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"},
19 | "mint": {:hex, :mint, "1.4.0", "cd7d2451b201fc8e4a8fd86257fb3878d9e3752899eb67b0c5b25b180bde1212", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "10a99e144b815cbf8522dccbc8199d15802440fc7a64d67b6853adb6fa170217"},
20 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"},
21 | "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},
22 | "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
23 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
24 | "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
25 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
26 | "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
27 | "xhttp": {:git, "https://github.com/ericmj/xhttp.git", "d0606462cba650fdb91b61e779f38e4e9b6383f6", []},
28 | }
29 |
--------------------------------------------------------------------------------
/test/headers_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Mojito.HeadersTest do
2 | use ExUnit.Case, async: true
3 | doctest Mojito.Headers
4 | alias Mojito.Headers
5 |
6 | @test_headers [
7 | {"header1", "value1"},
8 | {"header3", "value3-1"},
9 | {"header2", "value2"},
10 | {"HeaDer3", "value3-2"}
11 | ]
12 |
13 | test "Headers.get with no match" do
14 | assert(nil == Headers.get(@test_headers, "header0"))
15 | end
16 |
17 | test "Headers.get with case-sensitive match" do
18 | assert("value1" == Headers.get(@test_headers, "header1"))
19 | assert("value2" == Headers.get(@test_headers, "header2"))
20 | end
21 |
22 | test "Headers.get with case-insensitive match" do
23 | assert("value1" == Headers.get(@test_headers, "HEADER1"))
24 | assert("value2" == Headers.get(@test_headers, "hEaDeR2"))
25 | end
26 |
27 | test "Headers.get with multiple values" do
28 | assert("value3-1,value3-2" == Headers.get(@test_headers, "header3"))
29 | end
30 |
31 | test "Headers.get_values with no match" do
32 | assert([] == Headers.get_values(@test_headers, "header0"))
33 | end
34 |
35 | test "Headers.get_values with case-sensitive match" do
36 | assert(["value1"] == Headers.get_values(@test_headers, "header1"))
37 | assert(["value2"] == Headers.get_values(@test_headers, "header2"))
38 | end
39 |
40 | test "Headers.get_values with case-insensitive match" do
41 | assert(["value1"] == Headers.get_values(@test_headers, "HEADER1"))
42 | assert(["value2"] == Headers.get_values(@test_headers, "hEaDeR2"))
43 | end
44 |
45 | test "Headers.get_values with multiple values" do
46 | assert(
47 | ["value3-1", "value3-2"] == Headers.get_values(@test_headers, "header3")
48 | )
49 | end
50 |
51 | test "Headers.put when value doesn't exist" do
52 | output = [
53 | {"header1", "value1"},
54 | {"header3", "value3-1"},
55 | {"header2", "value2"},
56 | {"HeaDer3", "value3-2"},
57 | {"header4", "new value"}
58 | ]
59 |
60 | assert(output == Headers.put(@test_headers, "header4", "new value"))
61 | end
62 |
63 | test "Headers.put when value exists once" do
64 | output = [
65 | {"header1", "value1"},
66 | {"header3", "value3-1"},
67 | {"HeaDer3", "value3-2"},
68 | {"heADer2", "new value"}
69 | ]
70 |
71 | assert(output == Headers.put(@test_headers, "heADer2", "new value"))
72 | end
73 |
74 | test "Headers.put when value exists multiple times" do
75 | output = [
76 | {"header1", "value1"},
77 | {"header2", "value2"},
78 | {"HeaDer3", "new value"}
79 | ]
80 |
81 | assert(output == Headers.put(@test_headers, "HeaDer3", "new value"))
82 | end
83 |
84 | test "Headers.delete when value doesn't exist" do
85 | assert(@test_headers == Headers.delete(@test_headers, "nope"))
86 | end
87 |
88 | test "Headers.delete when value exists once" do
89 | output = [
90 | {"header1", "value1"},
91 | {"header3", "value3-1"},
92 | {"HeaDer3", "value3-2"}
93 | ]
94 |
95 | assert(output == Headers.delete(@test_headers, "heADer2"))
96 | end
97 |
98 | test "Headers.delete when value exists multiple times" do
99 | output = [
100 | {"header1", "value1"},
101 | {"header2", "value2"}
102 | ]
103 |
104 | assert(output == Headers.delete(@test_headers, "HEADER3"))
105 | end
106 |
107 | test "Headers.keys" do
108 | assert(["header1", "header3", "header2"] == Headers.keys(@test_headers))
109 | end
110 |
111 | test "normalize_headers" do
112 | output = [
113 | {"header1", "value1"},
114 | {"header3", "value3-1,value3-2"},
115 | {"header2", "value2"}
116 | ]
117 |
118 | assert(output == Headers.normalize(@test_headers))
119 | end
120 |
121 | test "convert_values_to_string converts numbers and atoms to string" do
122 | input = [
123 | {"integer", 2},
124 | {"float", 22.5},
125 | {"atom", :atom},
126 | {"list", [1, 2]},
127 | {"string", "string"}
128 | ]
129 |
130 | output = [
131 | {"integer", "2"},
132 | {"float", "22.5"},
133 | {"atom", "atom"},
134 | {"list", [1, 2]},
135 | {"string", "string"}
136 | ]
137 |
138 | assert(output == Headers.convert_values_to_string(input))
139 | end
140 | end
141 |
--------------------------------------------------------------------------------
/test/mojito_sync_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MojitoSyncTest do
2 | use ExSpec, async: false
3 | doctest Mojito
4 |
5 | context "local server tests" do
6 | @http_port Application.get_env(:mojito, :test_server_http_port)
7 |
8 | defp get(path, opts) do
9 | Mojito.get(
10 | "http://localhost:#{@http_port}#{path}",
11 | [],
12 | opts
13 | )
14 | end
15 |
16 | it "doesn't leak connections with pool: false" do
17 | original_open_ports = length(open_tcp_ports(@http_port))
18 | assert({:ok, response} = get("/", pool: false))
19 | assert(200 == response.status_code)
20 |
21 | final_open_ports = length(open_tcp_ports(@http_port))
22 | assert original_open_ports == final_open_ports
23 | end
24 | end
25 |
26 | defp open_tcp_ports(to_port) do
27 | Enum.filter(tcp_sockets(), fn socket ->
28 | case :inet.peername(socket) do
29 | {:ok, {_ip, ^to_port}} -> true
30 | _error -> false
31 | end
32 | end)
33 | end
34 |
35 | defp tcp_sockets() do
36 | Enum.filter(:erlang.ports(), fn port ->
37 | case :erlang.port_info(port, :name) do
38 | {_, 'tcp_inet'} -> true
39 | _ -> false
40 | end
41 | end)
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/test/mojito_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MojitoTest do
2 | use ExSpec, async: true
3 | doctest Mojito
4 | doctest Mojito.Utils
5 |
6 | alias Mojito.{Error, Headers}
7 |
8 | context "url validation" do
9 | it "fails on url without protocol" do
10 | assert({:error, _} = Mojito.request(:get, "localhost/path"))
11 | assert({:error, _} = Mojito.request(:get, "/localhost/path"))
12 | assert({:error, _} = Mojito.request(:get, "//localhost/path"))
13 | assert({:error, _} = Mojito.request(:get, "localhost//path"))
14 | end
15 |
16 | it "fails on url with bad protocol" do
17 | assert({:error, _} = Mojito.request(:get, "garbage://localhost/path"))
18 | assert({:error, _} = Mojito.request(:get, "ftp://localhost/path"))
19 | end
20 |
21 | it "fails on url without hostname" do
22 | assert({:error, _} = Mojito.request(:get, "http://"))
23 | end
24 |
25 | it "fails on blank url" do
26 | assert({:error, err} = Mojito.request(:get, ""))
27 | assert(is_binary(err.message))
28 | end
29 |
30 | it "fails on nil url" do
31 | assert({:error, err} = Mojito.request(:get, nil))
32 | assert(is_binary(err.message))
33 | end
34 | end
35 |
36 | context "method validation" do
37 | it "fails on blank method" do
38 | assert({:error, err} = Mojito.request("", "https://cool.com"))
39 | assert(is_binary(err.message))
40 | end
41 |
42 | it "fails on nil method" do
43 | assert({:error, err} = Mojito.request(nil, "https://cool.com"))
44 | assert(is_binary(err.message))
45 | end
46 | end
47 |
48 | context "local server tests" do
49 | @http_port Application.get_env(:mojito, :test_server_http_port)
50 | @https_port Application.get_env(:mojito, :test_server_https_port)
51 |
52 | defp head(path, opts \\ []) do
53 | Mojito.head(
54 | "http://localhost:#{@http_port}#{path}",
55 | [],
56 | opts
57 | )
58 | end
59 |
60 | defp get(path, opts \\ []) do
61 | Mojito.get(
62 | "http://localhost:#{@http_port}#{path}",
63 | [],
64 | opts
65 | )
66 | end
67 |
68 | defp get_with_user(path, user, opts \\ []) do
69 | Mojito.get(
70 | "http://#{user}@localhost:#{@http_port}#{path}",
71 | [],
72 | opts
73 | )
74 | end
75 |
76 | defp get_with_user_and_pass(path, user, pass, opts \\ []) do
77 | Mojito.get(
78 | "http://#{user}:#{pass}@localhost:#{@http_port}#{path}",
79 | [],
80 | opts
81 | )
82 | end
83 |
84 | defp post(path, body_obj, opts \\ []) do
85 | body = Jason.encode!(body_obj)
86 | headers = [{"content-type", "application/json"}]
87 |
88 | Mojito.post(
89 | "http://localhost:#{@http_port}#{path}",
90 | headers,
91 | body,
92 | opts
93 | )
94 | end
95 |
96 | defp put(path, body_obj, opts \\ []) do
97 | body = Jason.encode!(body_obj)
98 | headers = [{"content-type", "application/json"}]
99 |
100 | Mojito.put(
101 | "http://localhost:#{@http_port}#{path}",
102 | headers,
103 | body,
104 | opts
105 | )
106 | end
107 |
108 | defp patch(path, body_obj, opts \\ []) do
109 | body = Jason.encode!(body_obj)
110 | headers = [{"content-type", "application/json"}]
111 |
112 | Mojito.patch(
113 | "http://localhost:#{@http_port}#{path}",
114 | headers,
115 | body,
116 | opts
117 | )
118 | end
119 |
120 | defp delete(path, opts \\ []) do
121 | Mojito.delete(
122 | "http://localhost:#{@http_port}#{path}",
123 | [],
124 | opts
125 | )
126 | end
127 |
128 | defp options(path, opts \\ []) do
129 | Mojito.options(
130 | "http://localhost:#{@http_port}#{path}",
131 | [],
132 | opts
133 | )
134 | end
135 |
136 | defp get_ssl(path, opts \\ []) do
137 | Mojito.get(
138 | "https://localhost:#{@https_port}#{path}",
139 | [],
140 | [transport_opts: [verify: :verify_none]] ++ opts
141 | )
142 | end
143 |
144 | it "accepts kwlist input" do
145 | assert(
146 | {:ok, _response} =
147 | Mojito.request(method: :get, url: "http://localhost:#{@http_port}/")
148 | )
149 | end
150 |
151 | it "accepts pool: true" do
152 | assert(
153 | {:ok, _response} =
154 | Mojito.request(
155 | method: :get,
156 | url: "http://localhost:#{@http_port}/",
157 | opts: [pool: true]
158 | )
159 | )
160 | end
161 |
162 | it "accepts pool: false" do
163 | assert(
164 | {:ok, _response} =
165 | Mojito.request(
166 | method: :get,
167 | url: "http://localhost:#{@http_port}/",
168 | opts: [pool: false]
169 | )
170 | )
171 | end
172 |
173 | it "accepts pool: pid" do
174 | child_spec = Mojito.Pool.Poolboy.Single.child_spec()
175 | {:ok, pool_pid} = Supervisor.start_child(Mojito.Supervisor, child_spec)
176 |
177 | assert(
178 | {:ok, _response} =
179 | Mojito.request(
180 | method: :get,
181 | url: "http://localhost:#{@http_port}/",
182 | opts: [pool: pool_pid]
183 | )
184 | )
185 | end
186 |
187 | it "can make HTTP requests" do
188 | assert({:ok, response} = get("/"))
189 | assert(200 == response.status_code)
190 | assert("Hello world!" == response.body)
191 | assert(12 == response.size)
192 | assert("12" == Headers.get(response.headers, "content-length"))
193 | end
194 |
195 | it "can use HTTP/1.1" do
196 | assert({:ok, response} = get("/", protocols: [:http1]))
197 | assert(200 == response.status_code)
198 | assert("Hello world!" == response.body)
199 | assert(12 == response.size)
200 | assert("12" == Headers.get(response.headers, "content-length"))
201 | end
202 |
203 | it "can use HTTP/2" do
204 | assert({:ok, response} = get("/", protocols: [:http2]))
205 | assert(200 == response.status_code)
206 | assert("Hello world!" == response.body)
207 | assert(12 == response.size)
208 | assert("12" == Headers.get(response.headers, "content-length"))
209 | end
210 |
211 | it "can make HTTPS requests" do
212 | assert({:ok, response} = get_ssl("/"))
213 | assert(200 == response.status_code)
214 | assert("Hello world!" == response.body)
215 | assert(12 == response.size)
216 | assert("12" == Headers.get(response.headers, "content-length"))
217 | end
218 |
219 | it "handles timeouts" do
220 | assert({:ok, _} = get("/", timeout: 100))
221 | assert({:error, %Error{reason: :timeout}} = get("/wait1", timeout: 100))
222 | end
223 |
224 | it "handles timeouts even on long requests" do
225 | port = Application.get_env(:mojito, :test_server_http_port)
226 | {:ok, conn} = Mojito.Conn.connect("http://localhost:#{port}")
227 |
228 | mint_conn =
229 | Map.put(conn.conn, :request, %{
230 | ref: nil,
231 | state: :status,
232 | method: :get,
233 | version: nil,
234 | status: nil,
235 | headers_buffer: [],
236 | content_length: nil,
237 | connection: [],
238 | transfer_encoding: [],
239 | body: nil
240 | })
241 |
242 | conn = %{conn | conn: mint_conn}
243 |
244 | pid = self()
245 |
246 | spawn(fn ->
247 | socket = conn.conn.socket
248 | Process.sleep(30)
249 | send(pid, {:tcp, socket, "HTTP/1.1 200 OK\r\nserver: Cowboy"})
250 | Process.sleep(30)
251 | send(pid, {:tcp, socket, "\r\ndate: Thu, 25 Apr 2019 10:48:25"})
252 | Process.sleep(30)
253 | send(pid, {:tcp, socket, " GMT\r\ncontent-length: 12\r\ncache-"})
254 | Process.sleep(30)
255 | send(pid, {:tcp, socket, "control: max-age=0, private, must-"})
256 | Process.sleep(30)
257 | send(pid, {:tcp, socket, "revalidate\r\n\r\nHello world!"})
258 | end)
259 |
260 | assert(
261 | {:error, %{reason: :timeout}} =
262 | Mojito.Request.Single.receive_response(
263 | conn,
264 | %Mojito.Response{},
265 | 100
266 | )
267 | )
268 | end
269 |
270 | it "can set a max size" do
271 | assert(
272 | {:error, %Mojito.Error{message: nil, reason: :max_body_size_exceeded}} ==
273 | get("/infinite", timeout: 10000, max_body_size: 10)
274 | )
275 | end
276 |
277 | it "handles requests after a timeout" do
278 | assert({:error, %{reason: :timeout}} = get("/wait?d=10", timeout: 1))
279 | Process.sleep(100)
280 | assert({:ok, %{body: "Hello Alice!"}} = get("?name=Alice"))
281 | end
282 |
283 | it "handles URL query params" do
284 | assert({:ok, %{body: "Hello Alice!"}} = get("/?name=Alice"))
285 | assert({:ok, %{body: "Hello Alice!"}} = get("?name=Alice"))
286 | end
287 |
288 | it "can post data" do
289 | assert({:ok, response} = post("/post", %{name: "Charlie"}))
290 | resp_body = response.body |> Jason.decode!()
291 | assert("Charlie" == resp_body["name"])
292 | end
293 |
294 | it "handles user+pass in URL" do
295 | assert({:ok, %{status_code: 500}} = get("/auth"))
296 |
297 | assert(
298 | {:ok, %{status_code: 200} = response} = get_with_user("/auth", "hi")
299 | )
300 |
301 | assert(%{"user" => "hi", "pass" => nil} = Jason.decode!(response.body))
302 |
303 | assert(
304 | {:ok, %{status_code: 200} = response} =
305 | get_with_user_and_pass("/auth", "hi", "mom")
306 | )
307 |
308 | assert(%{"user" => "hi", "pass" => "mom"} = Jason.decode!(response.body))
309 | end
310 |
311 | it "can make HEAD request" do
312 | assert({:ok, response} = head("/"))
313 | assert(200 == response.status_code)
314 | assert("" == response.body)
315 | assert("12" == Headers.get(response.headers, "content-length"))
316 | end
317 |
318 | it "can make PATCH request" do
319 | assert({:ok, response} = patch("/patch", %{name: "Charlie"}))
320 | resp_body = response.body |> Jason.decode!()
321 | assert("Charlie" == resp_body["name"])
322 | end
323 |
324 | it "can make PUT request" do
325 | assert({:ok, response} = put("/put", %{name: "Charlie"}))
326 | resp_body = response.body |> Jason.decode!()
327 | assert("Charlie" == resp_body["name"])
328 | end
329 |
330 | it "can make DELETE request" do
331 | assert({:ok, response} = delete("/delete"))
332 | assert(200 == response.status_code)
333 | end
334 |
335 | it "can make OPTIONS request" do
336 | assert({:ok, response} = options("/"))
337 |
338 | assert(
339 | "OPTIONS, GET, HEAD, POST, PATCH, PUT, DELETE" ==
340 | Headers.get(response.headers, "allow")
341 | )
342 | end
343 |
344 | it "expands gzip" do
345 | assert({:ok, response} = get("/gzip"))
346 | assert("{\"ok\":true}\n" == response.body)
347 |
348 | assert({:ok, response} = get("/gzip", raw: true))
349 | assert("{\"ok\":true}\n" != response.body)
350 | end
351 |
352 | it "expands deflate" do
353 | assert({:ok, response} = get("/deflate"))
354 | assert("{\"ok\":true}\n" == response.body)
355 |
356 | assert({:ok, response} = get("/deflate", raw: true))
357 | assert("{\"ok\":true}\n" != response.body)
358 | end
359 |
360 | it "handles connection:close response" do
361 | assert({:ok, response} = get("/close", pool: false))
362 | assert("close" == response.body)
363 | end
364 |
365 | it "handles ssl connection:close response" do
366 | assert({:ok, response} = get_ssl("/close", pool: false))
367 | assert("close" == response.body)
368 | end
369 |
370 | it "can POST big bodies over HTTP/1" do
371 | big = String.duplicate("x", 5_000_000)
372 | body = %{name: big}
373 | assert({:ok, response} = post("/post", body, protocols: [:http1]))
374 | assert({:ok, map} = Jason.decode(response.body))
375 | assert(%{"name" => big} == map)
376 | end
377 |
378 | it "can POST big bodies over HTTP/2" do
379 | big = String.duplicate("é", 2_500_000)
380 | body = %{name: big}
381 | assert({:ok, response} = post("/post", body, protocols: [:http2]))
382 | assert({:ok, map} = Jason.decode(response.body))
383 | assert(%{"name" => big} == map)
384 | end
385 |
386 | it "handles response chunks arriving during stream_request_body" do
387 | ## sending a body this big will trigger a 500 error in Cowboy
388 | ## because we have not configured it otherwise
389 | big = String.duplicate("x", 100_000_000)
390 | body = %{name: big}
391 |
392 | assert(
393 | {:ok, response} =
394 | post("/post", body, protocols: [:http2], timeout: 10_000)
395 | )
396 |
397 | assert(500 == response.status_code)
398 | end
399 |
400 | it "handles timeouts during stream_request_body" do
401 | big = String.duplicate("x", 5_000_000)
402 | body = %{name: big}
403 |
404 | assert(
405 | {:error, %{reason: :timeout}} =
406 | post("/post", body, protocols: [:http2], timeout: 10)
407 | )
408 | end
409 | end
410 |
411 | context "external tests" do
412 | it "can make HTTPS requests using proper cert chain by default" do
413 | assert({:ok, _} = Mojito.request(:get, "https://github.com"))
414 | end
415 | end
416 | end
417 |
--------------------------------------------------------------------------------
/test/pool/poolboy/manager_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Pool.Poolboy.ManagerTest do
2 | use ExSpec, async: true
3 |
4 | context "calls" do
5 | it "implements get_pools" do
6 | assert(
7 | [] =
8 | GenServer.call(
9 | Mojito.Pool.Poolboy.Manager,
10 | {:get_pools, {"example.com", 80}}
11 | )
12 | )
13 | end
14 |
15 | it "implements get_pool_states" do
16 | assert(
17 | [] =
18 | GenServer.call(
19 | Mojito.Pool.Poolboy.Manager,
20 | {:get_pool_states, {"example.com", 80}}
21 | )
22 | )
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/test/pool/poolboy/single_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Pool.Poolboy.SingleTest do
2 | use ExSpec, async: true
3 | doctest Mojito.Pool.Poolboy.Single
4 | doctest Mojito.ConnServer
5 |
6 | context "Mojito.Pool.Single" do
7 | @http_port Application.get_env(:mojito, :test_server_http_port)
8 | @https_port Application.get_env(:mojito, :test_server_https_port)
9 |
10 | defp with_pool(fun) do
11 | rand = round(:rand.uniform() * 1_000_000_000)
12 | pool_name = "TestPool#{rand}" |> String.to_atom()
13 | {:ok, pid} = start_pool(pool_name, size: 2, max_overflow: 1)
14 | fun.(pool_name)
15 | GenServer.stop(pid)
16 | end
17 |
18 | defp start_pool(name, opts) do
19 | children = [Mojito.Pool.Poolboy.Single.child_spec([{:name, name} | opts])]
20 | Supervisor.start_link(children, strategy: :one_for_one)
21 | end
22 |
23 | defp get(pool, path, opts \\ []) do
24 | Mojito.Pool.Poolboy.Single.request(
25 | pool,
26 | :get,
27 | "http://localhost:#{@http_port}#{path}",
28 | [],
29 | "",
30 | opts
31 | )
32 | end
33 |
34 | defp get_ssl(pool, path, opts \\ []) do
35 | Mojito.Pool.Poolboy.Single.request(
36 | pool,
37 | :get,
38 | "https://localhost:#{@https_port}#{path}",
39 | [],
40 | "",
41 | [transport_opts: [verify: :verify_none]] ++ opts
42 | )
43 | end
44 |
45 | it "can make HTTP requests" do
46 | with_pool(fn pool_name ->
47 | assert({:ok, response} = get(pool_name, "/"))
48 | assert(200 == response.status_code)
49 | end)
50 | end
51 |
52 | it "can make HTTPS requests" do
53 | with_pool(fn pool_name ->
54 | assert({:ok, response} = get_ssl(pool_name, "/"))
55 | assert(200 == response.status_code)
56 | end)
57 | end
58 |
59 | it "can get a max body size error" do
60 | with_pool(fn pool_name ->
61 | assert(
62 | {:error, %Mojito.Error{message: nil, reason: :max_body_size_exceeded}} ==
63 | get(pool_name, "/infinite", max_body_size: 10)
64 | )
65 | end)
66 | end
67 |
68 | it "can saturate pool" do
69 | with_pool(fn pool_name ->
70 | spawn(fn -> get(pool_name, "/wait1") end)
71 | spawn(fn -> get(pool_name, "/wait1") end)
72 | spawn(fn -> get(pool_name, "/wait1") end)
73 | spawn(fn -> get(pool_name, "/wait1") end)
74 | :timer.sleep(100)
75 |
76 | assert(
77 | {:error, %{reason: :checkout_timeout}} =
78 | get(pool_name, "/wait1", timeout: 50)
79 | )
80 |
81 | ## 0 ready, 1 waiting, 3 in-progress
82 | assert({:full, 0, 1, 3} = :poolboy.status(pool_name))
83 |
84 | :timer.sleep(1000)
85 |
86 | ## 1 ready, 0 waiting, 1 in-progress (the one previously waiting)
87 | assert({:ready, 1, 0, 1} = :poolboy.status(pool_name))
88 |
89 | :timer.sleep(1000)
90 |
91 | ## 2 ready, 0 waiting, 0 in progress (all done)
92 | assert({:ready, 2, 0, 0} = :poolboy.status(pool_name))
93 | end)
94 | end
95 |
96 | it "retries request when connection was closed" do
97 | with_pool(fn pool_name ->
98 | 1..100
99 | |> Enum.each(fn _ ->
100 | assert({:ok, _resp} = get(pool_name, "/"))
101 | end)
102 | end)
103 | end
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/test/pool/poolboy_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Mojito.Pool.PoolboyTest do
2 | use ExSpec, async: false
3 | doctest Mojito.Pool.Poolboy
4 |
5 | context "Mojito.Pool" do
6 | @http_port Application.get_env(:mojito, :test_server_http_port)
7 | @https_port Application.get_env(:mojito, :test_server_https_port)
8 |
9 | defp get(path, opts \\ []) do
10 | Mojito.Pool.Poolboy.request(%Mojito.Request{
11 | method: :get,
12 | url: "http://localhost:#{@http_port}#{path}",
13 | opts: opts
14 | })
15 | end
16 |
17 | defp get_ssl(path, opts \\ []) do
18 | Mojito.Pool.Poolboy.request(%Mojito.Request{
19 | method: :get,
20 | url: "https://localhost:#{@https_port}#{path}",
21 | opts: [transport_opts: [verify: :verify_none]] ++ opts
22 | })
23 | end
24 |
25 | it "can make HTTP requests" do
26 | assert({:ok, response} = get("/"))
27 | assert(200 == response.status_code)
28 | end
29 |
30 | it "can make HTTPS requests" do
31 | assert({:ok, response} = get_ssl("/"))
32 | assert(200 == response.status_code)
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/test/support/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDwDCCAqgCCQCM+nz93AYZJzANBgkqhkiG9w0BAQsFADCBoDELMAkGA1UEBhMC
3 | VVMxFjAUBgNVBAgMDU1hc3NhY2h1c2V0dHMxDzANBgNVBAcMBkJvc3RvbjEQMA4G
4 | A1UECgwHQXBwY3VlczEfMB0GA1UECwwWRW5naW5lZXJpbmcgRGVwYXJ0bWVudDEU
5 | MBIGA1UEAwwLYXBwY3Vlcy5jb20xHzAdBgkqhkiG9w0BCQEWEHRlYW1AYXBwY3Vl
6 | cy5jb20wIBcNMTgwMzI1MTQxMTAzWhgPMjExODAzMDExNDExMDNaMIGgMQswCQYD
7 | VQQGEwJVUzEWMBQGA1UECAwNTWFzc2FjaHVzZXR0czEPMA0GA1UEBwwGQm9zdG9u
8 | MRAwDgYDVQQKDAdBcHBjdWVzMR8wHQYDVQQLDBZFbmdpbmVlcmluZyBEZXBhcnRt
9 | ZW50MRQwEgYDVQQDDAthcHBjdWVzLmNvbTEfMB0GCSqGSIb3DQEJARYQdGVhbUBh
10 | cHBjdWVzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMraxsKV
11 | XjFNEqgbaDHlaxm/ZCiSQj8oY/Q9FjeaRjGQNkupmDqaF6HvfPLWQ+dNkQin0t0T
12 | 16iwTjO047SgPvLd3F9zTAxy8cBdlZf4h8suZa+88Jg7otlEuQ4C47Le9iCUrkum
13 | AsWpZXOz3QZ//eoQjYX3V3byovct6mdGOF5gy4e1rCr97WcwaLNkA3upaitKr4tG
14 | 4UBoNAunrB/98NGntXjcNV8JEGUJLNHS3E/BADbIASwpNqX2vs6Yb64/VKUhOpVv
15 | SdoQbyyDkDDtmv9fpe+LtF5XC1lpIaMUPzwD9BXKJ1iUqouiBhlFBXyiPsxlwrWg
16 | BS0kdCv86cnMowkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAeRHRYGIw4odN6GkG
17 | d+sUNWSFtZ/Bnwv9KOBzzU4A5Rmmkv2dZ3XAC7CZqeozpqQl0pzyy2AAH8U6/YK3
18 | 0ztR91m3IvpU+SP/pAXhxgaBOd0vsYHrSejuB5Dlx3pLPR1jxTp0hRMFcbfg92py
19 | R/QNcvGMHBV6ASVPa5lXgll/lrKLHBqA3BdeUkQKySwo2yDVkAXL4FIfECigUcvx
20 | EOe6ZVKqRjjGevVqPUtDLRM2bEqNCPj+FbaX/xyLlGa5vLr4Ew13kHMPe6w/Y3ji
21 | PSXvWGfv6RufLDHuFUWAFzmmJKl05Ccr/9U+hq045WxbXGt9pz5GSMRErHzIowSS
22 | u6HvlQ==
23 | -----END CERTIFICATE-----
24 |
--------------------------------------------------------------------------------
/test/support/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDK2sbClV4xTRKo
3 | G2gx5WsZv2QokkI/KGP0PRY3mkYxkDZLqZg6mheh73zy1kPnTZEIp9LdE9eosE4z
4 | tOO0oD7y3dxfc0wMcvHAXZWX+IfLLmWvvPCYO6LZRLkOAuOy3vYglK5LpgLFqWVz
5 | s90Gf/3qEI2F91d28qL3LepnRjheYMuHtawq/e1nMGizZAN7qWorSq+LRuFAaDQL
6 | p6wf/fDRp7V43DVfCRBlCSzR0txPwQA2yAEsKTal9r7OmG+uP1SlITqVb0naEG8s
7 | g5Aw7Zr/X6Xvi7ReVwtZaSGjFD88A/QVyidYlKqLogYZRQV8oj7MZcK1oAUtJHQr
8 | /OnJzKMJAgMBAAECggEAYL1QyH8fOne9C/p2CEWWe+LwSwDlIuWKNXHkZIPoMb7K
9 | he7NMDVIS+vANLbGD0rIfc47Gz9ZO5NI2BPN+9fn7T6s18BOZily7QA0VRMq/1ST
10 | HeoG+zKFiQPjFLGAEU+PJR6CuITlEYqlXTZLk8v6NWPLejXouksgOKzm+nVccHTz
11 | frqn+tKoz4p560uHevqWqmXZNejY5A365SzYVZ9bQj/aPaM1bNVKrfj4hvc5KnSG
12 | FdVeQVZjMgLNbOZUTyF7XwqQNPc3l7LWNOvx4xXyywG79I32/GrXuYYOYwDtpJeP
13 | krmy7TQRIRIAozeuGY1s6nYdJ2FU056laL8aaFKD2QKBgQDlucHjcNc9he4sKEMf
14 | z01WFOPBgl5+16DDj3dwxDMp4HFJLieGo3ZekvPOuqJ/0A84H4xQvRyYLc+K0f+N
15 | D5wcatoyZyCb5qymt1tQSAZRsboGTaBD2jQ+mI87JXal3Qvgal0jVnBYCaSbJCy6
16 | vxma8aOsHTPq9M7RzlkYeorYuwKBgQDiDkAA1uVofGa1AGDWg7MA9jUZaoK6Z84u
17 | U1WFZqOy9aSaisepFKK0JyQj0heTDZ0JpfC9w0vQ5+1IAgFXvQLm+BhZhVlfT2MU
18 | Uv/tWll4HW5C4DRPWyZagMAmE5j5X3NdnX2laXAikKR3af5ZPqCFYpaliBoXyamm
19 | ClE+TZhJCwKBgQCA1UZpWVU8yami1gmfA1Fp31lDout/00nzorfnZAEVkSu3UM0V
20 | 8wJlU6Cr5XtQlsySOw8kEIrCxZ5JSjA5WfHA9iPcdH2TMTDOZrItOddhZXzgIBSr
21 | OOpn2IMrNn1t06PffYcyVD25Ad9wqj7zlEy12qJh2hbNw/FhNIo+8iqAFQKBgA9J
22 | h2qHHdyDDS8QZ3waS/C0tcKSQWT5wCfB2va6ijeABTGuUPJOQvKL8xW5D38SXJxa
23 | bH1ox6fJB3LnL9APKDMWdA8ZxYF8jObC9ivHAGXvF5XOM7tqHp3gNx5cFOxIWDTs
24 | gaK+DqdHwNeSg3Dlm1Vp5WYsXhddu+tOp0/fT30hAoGBAKUvLqOl1kdVL8L6a6zB
25 | dhRIQmo5IpzeJHhQRoR/vRjwWof9VKgDAOKPMUmDYqcIsDpoCdTxxmkrF3V2NCl2
26 | 74rBVnNOn3T1/kuyr6H18BPdaA5kRd5ufOdlGe+hEkGmsruuOf3yfgaD7/j6WkyV
27 | BvWGYnPUpINUhIoLnb23wndr
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/test/support/mojito_test_server.ex:
--------------------------------------------------------------------------------
1 | defmodule Mojito.TestServer do
2 | use Application
3 |
4 | def start(_type, _args) do
5 | children = [
6 | {Plug.Cowboy,
7 | scheme: :http,
8 | plug: Mojito.TestServer.PlugRouter,
9 | port: Application.get_env(:mojito, :test_server_http_port)},
10 | {Plug.Cowboy,
11 | scheme: :https,
12 | plug: Mojito.TestServer.PlugRouter,
13 | port: Application.get_env(:mojito, :test_server_https_port),
14 | keyfile: File.cwd!() <> "/test/support/key.pem",
15 | certfile: File.cwd!() <> "/test/support/cert.pem"}
16 | ]
17 |
18 | Supervisor.start_link(children, strategy: :one_for_one)
19 | end
20 | end
21 |
22 | defmodule Mojito.TestServer.PlugRouter do
23 | use Plug.Router
24 |
25 | plug(Plug.Head)
26 |
27 | plug(:match)
28 |
29 | plug(
30 | Plug.Parsers,
31 | parsers: [:json],
32 | pass: ["application/json"],
33 | json_decoder: Jason
34 | )
35 |
36 | plug(:dispatch)
37 |
38 | get "/" do
39 | name = conn.params["name"] || "world"
40 | send_resp(conn, 200, "Hello #{name}!")
41 | end
42 |
43 | get "/close" do
44 | {adapter, req} = conn.adapter
45 |
46 | {:ok, req} =
47 | :cowboy_req.reply(
48 | 200,
49 | %{"connection" => "close"},
50 | "close",
51 | req
52 | )
53 |
54 | %{conn | adapter: {adapter, req}}
55 | end
56 |
57 | post "/post" do
58 | name = conn.body_params["name"] || "Bob"
59 | send_resp(conn, 200, Jason.encode!(%{name: name}))
60 | end
61 |
62 | patch "/patch" do
63 | name = conn.body_params["name"] || "Bob"
64 | send_resp(conn, 200, Jason.encode!(%{name: name}))
65 | end
66 |
67 | put "/put" do
68 | name = conn.body_params["name"] || "Bob"
69 | send_resp(conn, 200, Jason.encode!(%{name: name}))
70 | end
71 |
72 | delete "/delete" do
73 | send_resp(conn, 200, "")
74 | end
75 |
76 | options _ do
77 | conn
78 | |> merge_resp_headers([
79 | {"Allow", "OPTIONS, GET, HEAD, POST, PATCH, PUT, DELETE"}
80 | ])
81 | |> send_resp(200, "")
82 | end
83 |
84 | get "/auth" do
85 | ["Basic " <> auth64] = Plug.Conn.get_req_header(conn, "authorization")
86 | creds = auth64 |> Base.decode64!() |> String.split(":", parts: 2)
87 | user = creds |> Enum.at(0)
88 | pass = creds |> Enum.at(1)
89 | send_resp(conn, 200, Jason.encode!(%{user: user, pass: pass}))
90 | end
91 |
92 | get "/wait" do
93 | delay = (conn.params["d"] || "100") |> String.to_integer()
94 | :timer.sleep(delay)
95 | send_resp(conn, 200, "ok")
96 | end
97 |
98 | get "/wait1" do
99 | :timer.sleep(1000)
100 | send_resp(conn, 200, "ok")
101 | end
102 |
103 | get "/wait10" do
104 | :timer.sleep(10000)
105 | send_resp(conn, 200, "ok")
106 | end
107 |
108 | get "/infinite" do
109 | Stream.unfold(send_chunked(conn, 200), fn
110 | conn ->
111 | {:ok, conn} = chunk(conn, "bytes")
112 | {nil, conn}
113 | end)
114 | |> Stream.run()
115 | end
116 |
117 | @gzip_body "H4sICOnTcF4AA3Jlc3BvbnNlAKtWys9WsiopKk2t5QIAiEF/wgwAAAA="
118 | |> Base.decode64!()
119 |
120 | get "/gzip" do
121 | conn
122 | |> put_resp_header("content-encoding", "gzip")
123 | |> put_resp_header("content-type", "application/json")
124 | |> send_resp(200, @gzip_body)
125 | end
126 |
127 | @deflate_body "eJyrVsrPVrIqKSpNreUCABr+BBs=" |> Base.decode64!()
128 |
129 | get "/deflate" do
130 | conn
131 | |> put_resp_header("content-encoding", "deflate")
132 | |> put_resp_header("content-type", "application/json")
133 | |> send_resp(200, @deflate_body)
134 | end
135 | end
136 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | Logger.remove_backend(:console)
2 |
3 | Mojito.TestServer.start([], [])
4 |
5 | if System.get_env("SLOW_TESTS"), do: :timer.sleep(1000)
6 |
7 | ExUnit.start()
8 |
--------------------------------------------------------------------------------