requests_ruby 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 98c916667505e2bb295cfd13cfd3640a9fb73fe285248115b1f612d2b562a008
4
+ data.tar.gz: e8dc6c58aa8999fdb4ae84040733b05b801140efcac65b7ab9f834f299c8c4e8
5
+ SHA512:
6
+ metadata.gz: 102260dfeff2781a3cdd337b9a3771e43ad865a1c2968090268202b3ae915320e84b9b2129e6d6ba4698303638c207aa0b49e814782cfd9ef1914718bec73b97
7
+ data.tar.gz: 3b3a024c7adc8e3a47d13289b9668d7ee8f1379c98d8e317f718be6b2be5525f4cfb39ab4821e839a2af20ec6645f15d71be04f6d4857e520a2ef4cea06ecc32
data/CHANGELOG.md ADDED
@@ -0,0 +1,63 @@
1
+ # Changelog
2
+
3
+ ## 2.1.0
4
+
5
+ Big cleanup pass. Split the single `lib/requests.rb` file into a proper
6
+ `lib/requests/*` structure, fixed several real bugs, and filled in a bunch
7
+ of gaps vs. the python `requests` API.
8
+
9
+ ### Fixed
10
+ - **`Content-Type` header silently dropped on redirects.** The request body
11
+ was only built once, but the header carrying its `Content-Type` was set
12
+ on a headers hash that got thrown away and rebuilt on every redirect hop.
13
+ A 307/308 redirect that correctly preserved the POST body would send it
14
+ with no `Content-Type` at all. Body + content type are now computed once
15
+ and carried through the whole redirect chain.
16
+ - **Binary multipart uploads could raise `Encoding::CompatibilityError`.**
17
+ The multipart body was being built as a UTF-8 string; appending raw
18
+ binary file content (an image, a zip, whatever) onto it blew up the
19
+ moment the bytes weren't valid UTF-8. The body is now built as
20
+ `ASCII-8BIT` from the start.
21
+ - **Request bodies silently disappeared on GET/DELETE.** The code only
22
+ attached a body when `request_body_permitted?` was true, which is `false`
23
+ for GET in net/http. Some APIs (Elasticsearch, various search/query
24
+ APIs) intentionally send a JSON body with GET - that's now respected.
25
+ - **Confusing `unexpected token` errors from `Response#json`.** Calling
26
+ `.json` on a response that came back as an HTML error/login page (very
27
+ common when you hit a WAF, an expired-auth redirect, or the wrong host)
28
+ raised a bare `JSON::ParserError`-wrapped message with no context. The
29
+ error now says plainly that it looks like an HTML page, includes the
30
+ status code, and shows a truncated preview of the body.
31
+ - Deflate-compressed bodies missing the zlib header (some servers send raw
32
+ deflate) now fall back to a raw inflate instead of raising.
33
+ - `Requests::Timeout` / `ConnectTimeout` / `ReadTimeout` now share a
34
+ `Requests::Timeoutable` module so you can `rescue Requests::Timeoutable`
35
+ and catch both connect and read timeouts in one line, closer to python's
36
+ `except requests.Timeout`.
37
+
38
+ ### Added
39
+ - `Response#status` (alias of `status_code`), `#is_redirect?`,
40
+ `#is_permanent_redirect?`, `#client_error?`, `#server_error?`.
41
+ - `Response#save_to(path)` to stream a body straight to disk.
42
+ - `Response#request` is now a small `PreparedRequest` object (still
43
+ supports `resp.request[:url]` hash-style access for old code).
44
+ - `Session#mount(prefix, adapter)` + `Requests::HTTPAdapter` - swap out the
45
+ transport layer per URL prefix, loosely modeled on python's adapters.
46
+ - Built-in retry support: `retries:`/`Session#retries` and
47
+ `backoff_factor:` for transient connection errors on idempotent methods.
48
+ - `Session#get!` / `#post!` - like `get`/`post` but raise immediately on
49
+ a non-2xx/3xx response.
50
+ - `Requests.download(url, to: path)` convenience method.
51
+ - `Requests::InvalidSchema` (scheme present but not http/https) as a
52
+ companion to the existing `MissingSchema` (no scheme at all).
53
+ - Cross-host redirect now strips the `Authorization` header instead of
54
+ forwarding your bearer token / basic auth to whatever host the
55
+ `Location` header pointed at.
56
+ - A real test suite under `spec/` (plain minitest, no network calls).
57
+ - `Makefile` with `test`, `console`, `build`, `install`, `release`, `lint`.
58
+ - `Requests::CaseInsensitiveDict` / `Requests::CookieJar` aliases for
59
+ people coming from the python naming.
60
+
61
+ ## 1.0.0 / 2.0.0
62
+
63
+ Initial single-file releases.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 requests_ruby contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,331 @@
1
+ # requests_ruby
2
+
3
+ A zero-dependency, stdlib-only HTTP client for Ruby, modeled closely on Python's beloved [`requests`](https://requests.readthedocs.io/) library. No `Gemfile`, no C extensions, no transitive dependency tree — just Ruby's own `net/http` wrapped in an API that doesn't make you want to throw your laptop.
4
+
5
+ [![Gem Version](https://img.shields.io/badge/gem-requests__ruby-red?logo=rubygems)](https://rubygems.org/gems/requests_ruby)
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7-CC342D?logo=ruby)](https://www.ruby-lang.org)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-zero-brightgreen)](requests_ruby.gemspec)
9
+
10
+ ---
11
+
12
+ ## Table of Contents
13
+
14
+ - [Why this exists](#-why-this-exists)
15
+ - [Installation](#-installation)
16
+ - [Quick start](#-quick-start)
17
+ - [Feature tour](#-feature-tour)
18
+ - [Query params](#query-params)
19
+ - [Sending data (form / JSON / files)](#sending-data-form--json--files)
20
+ - [Response object](#response-object)
21
+ - [Sessions](#sessions)
22
+ - [Authentication](#authentication)
23
+ - [Errors & exceptions](#errors--exceptions)
24
+ - [Redirects & history](#redirects--history)
25
+ - [Timeouts & retries](#timeouts--retries)
26
+ - [Proxies & SSL](#proxies--ssl)
27
+ - [Cookies](#cookies)
28
+ - [Hooks](#hooks)
29
+ - [Streaming & downloads](#streaming--downloads)
30
+ - [Custom transport adapters](#custom-transport-adapters)
31
+ - [Status codes](#status-codes)
32
+ - [API reference](#-api-reference)
33
+ - [Python parity cheat sheet](#-python-parity-cheat-sheet)
34
+ - [Known limitations](#-known-limitations)
35
+ - [Development](#-development)
36
+ - [Contributing](#-contributing)
37
+ - [License](#-license)
38
+
39
+ ---
40
+
41
+ ## Why this exists
42
+
43
+ Ruby has plenty of great HTTP gems (`Faraday`, `HTTParty`, `httpx`...) but most either pull in dependencies or don't quite match the ergonomics people already know from Python's `requests`. This gem is for the specific case of: *"I want `requests.get(url).json()` energy, in Ruby, using only what ships with Ruby itself."*
44
+
45
+ If you're coming from Python, the [parity cheat sheet](#-python-parity-cheat-sheet) below will feel like home.
46
+
47
+ ## Installation
48
+
49
+ Add it to your `Gemfile`:
50
+
51
+ ```ruby
52
+ gem 'requests_ruby'
53
+ ```
54
+
55
+ Or install it directly:
56
+
57
+ ```bash
58
+ gem install requests_ruby
59
+ ```
60
+
61
+ Then, in your code — `require 'requests'` works out of the box (matching the ergonomics of the gem's underlying `Requests` module), and so does `require 'requests_ruby'` if you'd rather match the gem name exactly. Both point at the same code:
62
+
63
+ ```ruby
64
+ require 'requests'
65
+ # -- or --
66
+ require 'requests_ruby'
67
+ ```
68
+
69
+ ## Quick start
70
+
71
+ ```ruby
72
+ require 'requests'
73
+
74
+ r = Requests.get('https://api.example.com/users', params: { page: 1 })
75
+
76
+ r.status_code #=> 200
77
+ r.ok? #=> true
78
+ r.json #=> { "users" => [...] }
79
+ r.text #=> "{\"users\":[...]}"
80
+ r.headers['content-type']
81
+ ```
82
+
83
+ ## Feature tour
84
+
85
+ ### Query params
86
+
87
+ ```ruby
88
+ Requests.get('https://api.example.com/search', params: { q: 'ruby gems', page: 2 })
89
+ # => https://api.example.com/search?q=ruby+gems&page=2
90
+
91
+ # arrays become repeated params
92
+ Requests.get('https://api.example.com/search', params: { tag: ['ruby', 'http'] })
93
+ # => ...?tag=ruby&tag=http
94
+ ```
95
+
96
+ ### Sending data (form / JSON / files)
97
+
98
+ ```ruby
99
+ # JSON body
100
+ Requests.post('https://api.example.com/login', json: { user: 'ali', pass: 'secret' })
101
+
102
+ # form-encoded body
103
+ Requests.post('https://api.example.com/login', data: { user: 'ali', pass: 'secret' })
104
+
105
+ # multipart file upload
106
+ Requests.post('https://api.example.com/upload',
107
+ data: { title: 'my photo' },
108
+ files: { avatar: ['pic.png', File.read('pic.png'), 'image/png'] })
109
+ ```
110
+
111
+ ### Response object
112
+
113
+ ```ruby
114
+ r = Requests.get(url)
115
+
116
+ r.status_code # 200
117
+ r.status # alias for status_code
118
+ r.ok? # true if < 400
119
+ r.text # decoded body as a UTF-8 string
120
+ r.content # raw body
121
+ r.json # parsed JSON (raises Requests::JSONDecodeError if it isn't JSON)
122
+ r.headers['etag'] # case-insensitive header access
123
+ r.cookies # cookie jar collected across the request
124
+ r.elapsed # Float seconds
125
+ r.url # final URL after any redirects
126
+ r.history # array of intermediate Response objects
127
+ r.links # parsed Link header, e.g. pagination
128
+ r.request # PreparedRequest - what was actually sent (method/url/headers/body)
129
+ r.redirect? # true for 3xx with a Location header
130
+ r.client_error? # 400-499
131
+ r.server_error? # 500+
132
+ r.save_to('file.bin') # stream raw_body straight to disk
133
+ ```
134
+
135
+ ### Sessions
136
+
137
+ Sessions persist headers, cookies, and auth across multiple requests to the same service — and reuse is not just convenient, it avoids re-doing DNS/TLS handshakes on every call.
138
+
139
+ ```ruby
140
+ s = Requests::Session.new
141
+ s.headers['Authorization'] = 'Bearer xyz'
142
+
143
+ s.get('https://api.example.com/me')
144
+ s.get('https://api.example.com/orders')
145
+
146
+ # raise immediately instead of checking status_code yourself
147
+ s.get!('https://api.example.com/me')
148
+ ```
149
+
150
+ ### Authentication
151
+
152
+ ```ruby
153
+ Requests.get(url, auth: Requests::BasicAuth.new('user', 'pass'))
154
+ Requests.get(url, auth: Requests::BearerAuth.new('token'))
155
+ Requests.get(url, auth: Requests::DigestAuth.new('user', 'pass'))
156
+ ```
157
+
158
+ Write your own by implementing `#call(headers)` — anything that responds to it works as an `auth:` value.
159
+
160
+ ### Errors & exceptions
161
+
162
+ ```ruby
163
+ begin
164
+ r = Requests.get(url)
165
+ r.raise_for_status
166
+ rescue Requests::HTTPError => e
167
+ puts e.message # "404 Client Error: Not Found for url: ..."
168
+ puts e.response.status_code
169
+ rescue Requests::Timeoutable
170
+ # catches BOTH Requests::ConnectTimeout and Requests::ReadTimeout
171
+ puts 'timed out'
172
+ rescue Requests::ConnectionError => e
173
+ puts e.message
174
+ end
175
+ ```
176
+
177
+ Exception hierarchy at a glance:
178
+
179
+ ```
180
+ Requests::RequestException
181
+ ├── Requests::HTTPError
182
+ ├── Requests::ConnectionError
183
+ │ ├── Requests::ProxyError
184
+ │ ├── Requests::SSLError
185
+ │ └── Requests::ConnectTimeout (also includes Requests::Timeoutable)
186
+ ├── Requests::Timeout (also includes Requests::Timeoutable)
187
+ │ └── Requests::ReadTimeout
188
+ ├── Requests::URLRequired
189
+ ├── Requests::TooManyRedirects
190
+ ├── Requests::MissingSchema (no scheme at all, e.g. "example.com")
191
+ ├── Requests::InvalidSchema (unsupported scheme, e.g. "ftp://")
192
+ ├── Requests::InvalidURL
193
+ │ └── Requests::InvalidProxyURL
194
+ ├── Requests::ChunkedEncodingError
195
+ ├── Requests::ContentDecodingError
196
+ ├── Requests::StreamConsumedError
197
+ ├── Requests::InvalidHeader
198
+ ├── Requests::RetryError
199
+ └── Requests::InvalidJSONError
200
+ └── Requests::JSONDecodeError
201
+ ```
202
+
203
+ ### Redirects & history
204
+
205
+ ```ruby
206
+ r = Requests.get(url)
207
+ r.history # every intermediate 3xx Response, in order
208
+ r.url # the final URL you landed on
209
+
210
+ r = Requests.get(url, allow_redirects: false) # don't follow at all
211
+ ```
212
+
213
+ ### Timeouts & retries
214
+
215
+ ```ruby
216
+ Requests.get(url, timeout: 5) # 5s for both connect and read
217
+ Requests.get(url, timeout: [3, 10]) # [connect, read]
218
+
219
+ # retry transient connection errors on idempotent methods (GET/HEAD/OPTIONS/PUT/DELETE)
220
+ s = Requests::Session.new
221
+ s.retries = 3
222
+ s.backoff_factor = 0.5 # sleeps 0.5s, 1s, 1.5s between attempts
223
+ s.get(url)
224
+
225
+ # or per-request
226
+ Requests.get(url, retries: 3)
227
+ ```
228
+
229
+ ### Proxies & SSL
230
+
231
+ ```ruby
232
+ Requests.get(url, proxies: { 'http' => 'http://127.0.0.1:8080', 'https' => 'http://127.0.0.1:8080' })
233
+ Requests.get(url, verify: false) # skip cert verification (careful!)
234
+ Requests.get(url, verify: '/path/to/ca.pem') # custom CA bundle
235
+ ```
236
+
237
+ A bundled `cacert.pem` (Mozilla's CA bundle) ships with the gem and is used by default. Override it globally with the `REQUESTS_CA_FILE` environment variable.
238
+
239
+ ### Cookies
240
+
241
+ ```ruby
242
+ r = Requests.get(url)
243
+ r.cookies['session_id']
244
+
245
+ s = Requests::Session.new
246
+ s.cookies.set('theme', 'dark')
247
+ s.get(url) # sent automatically on every request through this session
248
+ ```
249
+
250
+ ### Hooks
251
+
252
+ ```ruby
253
+ s = Requests::Session.new
254
+ s.hooks[:response] << ->(resp) { puts "#{resp.request[:method]} #{resp.url} -> #{resp.status_code}" }
255
+ s.get(url)
256
+ ```
257
+
258
+ ### Streaming & downloads
259
+
260
+ ```ruby
261
+ r = Requests.get(url)
262
+ r.iter_content(chunk_size: 4096) { |chunk| ... }
263
+ r.iter_lines { |line| ... }
264
+
265
+ # convenience one-liner for "just save this to disk"
266
+ Requests.download('https://example.com/file.zip', to: 'file.zip')
267
+ ```
268
+
269
+ > ℹ️ Note: unlike Python's `requests`, the body is fully read before `iter_content`/`iter_lines` yield anything — this library deliberately stays stdlib-only, and true lazy streaming would need extra machinery to keep a `net/http` connection open across method calls. See [Known limitations](#-known-limitations).
270
+
271
+ ### Custom transport adapters
272
+
273
+ Loosely inspired by Python's `Session.mount()` / `HTTPAdapter`:
274
+
275
+ ```ruby
276
+ class LoggingAdapter < Requests::HTTPAdapter
277
+ def send_once(*args)
278
+ puts 'sending a request...'
279
+ super
280
+ end
281
+ end
282
+
283
+ s = Requests::Session.new
284
+ s.mount('https://internal.example.com/', LoggingAdapter.new)
285
+ ```
286
+
287
+ ### Status codes
288
+
289
+ ```ruby
290
+ Requests.codes.ok # 200
291
+ Requests.codes.not_found # 404
292
+ Requests.codes.im_a_teapot # 418, yes really
293
+ ```
294
+
295
+ ## API reference
296
+
297
+ | Module-level | Session | Response |
298
+ |---|---|---|
299
+ | `Requests.get/post/put/patch/delete/head/options` | `session.get/post/put/patch/delete/head/options` | `status_code`, `status` |
300
+ | `Requests.request(method, url, **opts)` | `session.request(method, url, **opts)` | `ok?`, `redirect?` |
301
+ | `Requests.session` → new `Session` | `session.get!/post!` (raise on error) | `text`, `content`, `json` |
302
+ | `Requests.download(url, to:)` | `session.mount(prefix, adapter)` | `headers`, `cookies` |
303
+ | `Requests.codes` | `session.hooks[:response]` | `elapsed`, `history`, `url` |
304
+ | | `session.headers`, `.cookies`, `.auth`, `.proxies`, `.retries` | `raise_for_status`, `save_to`, `links` |
305
+
306
+ Every request-level method accepts: `params`, `data`, `json`, `headers`, `cookies`, `files`, `auth`, `timeout`, `allow_redirects`, `proxies`, `verify`, `cert`, `hooks`, `retries`.
307
+
308
+
309
+ ## 🔧 Development
310
+
311
+ ```bash
312
+ git clone https://github.com/requests-ruby/requests_ruby.git
313
+ cd requests_ruby
314
+ make test # run the test suite
315
+ make console # irb with the lib pre-loaded
316
+ make build # build the .gem into pkg/
317
+ ```
318
+
319
+ See the [Makefile](Makefile) for the full list of targets.
320
+
321
+ ## Contributing
322
+
323
+ Bug reports and pull requests are welcome. A few ground rules:
324
+
325
+ 1. No new runtime dependencies — this stays stdlib-only, on purpose.
326
+ 2. Add a test in `spec/requests_spec.rb` for anything you fix or add.
327
+ 3. Keep the Python-parity naming where it makes sense, but don't force it where it doesn't fit Ruby idiom (`?`-suffixed predicates, etc.).
328
+
329
+ ## License
330
+
331
+ [MIT](LICENSE)