philiprehberger-http_client 0.1.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: d11769686f37911e9a4273468d8ae3d525162853eef921c26fa1f73b73e23a93
4
+ data.tar.gz: 0f290ad715f1ed058381676b87e142c7c239fb8aefcd4c4b3e5bb833391017f7
5
+ SHA512:
6
+ metadata.gz: ce4fe073d9cc426661e965f6c1c95e613265a82db24eebc20d82624107b511e6d9f07fd630e8740666895ce6ce6b3b573bc719b19dfccf56ed8163a058bbf4a5
7
+ data.tar.gz: 87f238755a902ff293774f7328697caf14b76f119545f7c1aa41525dadc7fd1753690d12c05d18d55836cec60a08e7f4f1a28fb76667ca824c55d429b675bc61
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-03-10
9
+
10
+ ### Added
11
+
12
+ - Initial release
13
+ - HTTP methods: GET, POST, PUT, PATCH, DELETE
14
+ - Automatic retries on network errors with configurable delay
15
+ - Request/response interceptors via `use` block
16
+ - JSON request and response helpers
17
+ - Response wrapper with `ok?` and `json` convenience methods
18
+ - Zero dependencies — built on Ruby stdlib `net/http`
19
+
20
+ [0.1.0]: https://github.com/philiprehberger/rb-http-client/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 philiprehberger
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,127 @@
1
+ # philiprehberger-http_client
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/philiprehberger-http_client.svg)](https://badge.fury.io/rb/philiprehberger-http_client)
4
+ [![CI](https://github.com/philiprehberger/rb-http-client/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-http-client/actions/workflows/ci.yml)
5
+
6
+ Lightweight HTTP client wrapper with retries and interceptors. Zero dependencies — built on Ruby's stdlib `net/http`.
7
+
8
+ ## Installation
9
+
10
+ Add to your Gemfile:
11
+
12
+ ```ruby
13
+ gem "philiprehberger-http_client"
14
+ ```
15
+
16
+ Or install directly:
17
+
18
+ ```sh
19
+ gem install philiprehberger-http_client
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ruby
25
+ require "philiprehberger/http_client"
26
+
27
+ client = Philiprehberger::HttpClient.new(base_url: "https://api.example.com")
28
+ ```
29
+
30
+ ### GET request
31
+
32
+ ```ruby
33
+ response = client.get("/users", params: { page: 1 })
34
+
35
+ puts response.status # => 200
36
+ puts response.ok? # => true
37
+ puts response.json # => [{"id" => 1, "name" => "Alice"}, ...]
38
+ ```
39
+
40
+ ### POST with JSON body
41
+
42
+ ```ruby
43
+ response = client.post("/users", json: { name: "Bob", email: "bob@example.com" })
44
+
45
+ puts response.status # => 201
46
+ puts response.json # => {"id" => 2, "name" => "Bob", ...}
47
+ ```
48
+
49
+ ### Default headers
50
+
51
+ ```ruby
52
+ client = Philiprehberger::HttpClient.new(
53
+ base_url: "https://api.example.com",
54
+ headers: { "Authorization" => "Bearer token123" }
55
+ )
56
+ ```
57
+
58
+ ### Interceptors
59
+
60
+ Add request/response interceptors to log, modify, or inspect traffic:
61
+
62
+ ```ruby
63
+ client = Philiprehberger::HttpClient.new(base_url: "https://api.example.com")
64
+
65
+ client.use do |context|
66
+ if context[:response]
67
+ puts "Response: #{context[:response].status}"
68
+ else
69
+ puts "Request: #{context[:request][:method]} #{context[:request][:uri]}"
70
+ end
71
+ end
72
+
73
+ client.get("/health")
74
+ # Prints:
75
+ # Request: GET https://api.example.com/health
76
+ # Response: 200
77
+ ```
78
+
79
+ ### Retries
80
+
81
+ Automatically retry on network errors (connection refused, timeouts, etc.):
82
+
83
+ ```ruby
84
+ client = Philiprehberger::HttpClient.new(
85
+ base_url: "https://api.example.com",
86
+ retries: 3,
87
+ retry_delay: 2
88
+ )
89
+
90
+ response = client.get("/unstable-endpoint")
91
+ ```
92
+
93
+ ### All HTTP methods
94
+
95
+ ```ruby
96
+ client.get("/resource", params: { q: "search" })
97
+ client.post("/resource", json: { key: "value" })
98
+ client.put("/resource/1", json: { key: "updated" })
99
+ client.patch("/resource/1", json: { key: "patched" })
100
+ client.delete("/resource/1")
101
+ ```
102
+
103
+ ## API
104
+
105
+ ### `Philiprehberger::HttpClient.new(**options)`
106
+
107
+ | Option | Type | Default | Description |
108
+ |---------------|---------|---------|--------------------------------------|
109
+ | `base_url` | String | — | Base URL for all requests (required) |
110
+ | `headers` | Hash | `{}` | Default headers for every request |
111
+ | `timeout` | Integer | `30` | Read/open timeout in seconds |
112
+ | `retries` | Integer | `0` | Retry attempts on network errors |
113
+ | `retry_delay` | Numeric | `1` | Seconds between retries |
114
+
115
+ ### `Response`
116
+
117
+ | Method | Returns | Description |
118
+ |-----------|---------|---------------------------------|
119
+ | `status` | Integer | HTTP status code |
120
+ | `body` | String | Raw response body |
121
+ | `headers` | Hash | Response headers |
122
+ | `ok?` | Boolean | `true` if status is 200-299 |
123
+ | `json` | Hash | Parsed JSON body |
124
+
125
+ ## License
126
+
127
+ [MIT](LICENSE)
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module Philiprehberger
8
+ module HttpClient
9
+ class Client
10
+ include Connection
11
+
12
+ # @param base_url [String] Base URL for all requests
13
+ # @param headers [Hash] Default headers applied to every request
14
+ # @param timeout [Integer] Read/open timeout in seconds
15
+ # @param retries [Integer] Number of retry attempts on network errors
16
+ # @param retry_delay [Numeric] Seconds to wait between retries
17
+ def initialize(base_url:, headers: {}, timeout: 30, retries: 0, retry_delay: 1)
18
+ @base_url = base_url.chomp("/")
19
+ @default_headers = headers
20
+ @timeout = timeout
21
+ @retries = retries
22
+ @retry_delay = retry_delay
23
+ @interceptors = []
24
+ end
25
+
26
+ # Register a request/response interceptor.
27
+ #
28
+ # The block receives a Hash with :request and, after the request completes, :response.
29
+ # It is called twice: once before the request (with :request only) and once after
30
+ # (with both :request and :response).
31
+ #
32
+ # @yield [Hash] context hash with :request and optionally :response
33
+ # @return [self]
34
+ def use(&block)
35
+ @interceptors << block
36
+ self
37
+ end
38
+
39
+ # Perform a GET request.
40
+ #
41
+ # @param path [String] Request path appended to the base URL
42
+ # @param params [Hash] Query parameters
43
+ # @param headers [Hash] Additional headers for this request
44
+ # @return [Response]
45
+ def get(path, params: {}, headers: {})
46
+ uri = build_uri(path, params)
47
+ request = Net::HTTP::Get.new(uri)
48
+ execute(uri, request, headers)
49
+ end
50
+
51
+ # Perform a POST request.
52
+ #
53
+ # @param path [String] Request path
54
+ # @param body [String, nil] Raw body string
55
+ # @param json [Hash, Array, nil] JSON-serializable body (sets Content-Type automatically)
56
+ # @param headers [Hash] Additional headers
57
+ # @return [Response]
58
+ def post(path, body: nil, json: nil, headers: {})
59
+ uri = build_uri(path)
60
+ request = Net::HTTP::Post.new(uri)
61
+ set_body(request, body, json, headers)
62
+ execute(uri, request, headers)
63
+ end
64
+
65
+ # Perform a PUT request.
66
+ #
67
+ # @param path [String] Request path
68
+ # @param body [String, nil] Raw body string
69
+ # @param json [Hash, Array, nil] JSON-serializable body
70
+ # @param headers [Hash] Additional headers
71
+ # @return [Response]
72
+ def put(path, body: nil, json: nil, headers: {})
73
+ uri = build_uri(path)
74
+ request = Net::HTTP::Put.new(uri)
75
+ set_body(request, body, json, headers)
76
+ execute(uri, request, headers)
77
+ end
78
+
79
+ # Perform a PATCH request.
80
+ #
81
+ # @param path [String] Request path
82
+ # @param body [String, nil] Raw body string
83
+ # @param json [Hash, Array, nil] JSON-serializable body
84
+ # @param headers [Hash] Additional headers
85
+ # @return [Response]
86
+ def patch(path, body: nil, json: nil, headers: {})
87
+ uri = build_uri(path)
88
+ request = Net::HTTP::Patch.new(uri)
89
+ set_body(request, body, json, headers)
90
+ execute(uri, request, headers)
91
+ end
92
+
93
+ # Perform a DELETE request.
94
+ #
95
+ # @param path [String] Request path
96
+ # @param headers [Hash] Additional headers
97
+ # @return [Response]
98
+ def delete(path, headers: {})
99
+ uri = build_uri(path)
100
+ request = Net::HTTP::Delete.new(uri)
101
+ execute(uri, request, headers)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module HttpClient
5
+ # Internal helpers for building URIs, HTTP connections, executing requests,
6
+ # and constructing Response objects. Mixed into Client to keep it concise.
7
+ module Connection
8
+ private
9
+
10
+ def build_uri(path, params = {})
11
+ url = "#{@base_url}/#{path.sub(%r{^/}, '')}"
12
+ uri = URI.parse(url)
13
+ unless params.empty?
14
+ query = URI.encode_www_form(params)
15
+ uri.query = uri.query ? "#{uri.query}&#{query}" : query
16
+ end
17
+ uri
18
+ end
19
+
20
+ def set_body(request, body, json_body, headers)
21
+ if json_body
22
+ request.body = JSON.generate(json_body)
23
+ headers["content-type"] ||= "application/json"
24
+ elsif body
25
+ request.body = body
26
+ end
27
+ end
28
+
29
+ def apply_headers(request, extra_headers)
30
+ merged = @default_headers.merge(extra_headers)
31
+ merged.each { |key, value| request[key] = value }
32
+ end
33
+
34
+ def execute(uri, request, extra_headers)
35
+ apply_headers(request, extra_headers)
36
+
37
+ context = { request: { uri: uri, method: request.method, headers: request.to_hash } }
38
+ run_interceptors(context)
39
+
40
+ response = perform_with_retries(uri, request)
41
+ context[:response] = response
42
+ run_interceptors(context)
43
+
44
+ response
45
+ end
46
+
47
+ def perform_with_retries(uri, request)
48
+ attempts = 0
49
+ begin
50
+ perform_request(uri, request)
51
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT,
52
+ Net::OpenTimeout, Net::ReadTimeout, SocketError => e
53
+ attempts += 1
54
+ raise e unless attempts <= @retries
55
+
56
+ sleep(@retry_delay)
57
+ retry
58
+ end
59
+ end
60
+
61
+ def perform_request(uri, request)
62
+ http = build_http(uri)
63
+ raw = http.request(request)
64
+ build_response(raw)
65
+ end
66
+
67
+ def build_http(uri)
68
+ http = Net::HTTP.new(uri.host, uri.port)
69
+ http.use_ssl = uri.scheme == "https"
70
+ http.open_timeout = @timeout
71
+ http.read_timeout = @timeout
72
+ http
73
+ end
74
+
75
+ def build_response(raw)
76
+ response_headers = {}
77
+ raw.each_header { |k, v| response_headers[k] = v }
78
+
79
+ Response.new(
80
+ status: raw.code.to_i,
81
+ body: raw.body || "",
82
+ headers: response_headers
83
+ )
84
+ end
85
+
86
+ def run_interceptors(context)
87
+ @interceptors.each { |interceptor| interceptor.call(context) }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Philiprehberger
6
+ module HttpClient
7
+ class Response
8
+ attr_reader :status, :body, :headers
9
+
10
+ # @param status [Integer] HTTP status code
11
+ # @param body [String] Response body
12
+ # @param headers [Hash] Response headers
13
+ def initialize(status:, body:, headers: {})
14
+ @status = status
15
+ @body = body
16
+ @headers = headers
17
+ end
18
+
19
+ # Returns true if the status code is in the 2xx range.
20
+ #
21
+ # @return [Boolean]
22
+ def ok?
23
+ status >= 200 && status < 300
24
+ end
25
+
26
+ # Parses the response body as JSON.
27
+ #
28
+ # @return [Hash, Array] Parsed JSON
29
+ # @raise [JSON::ParserError] If the body is not valid JSON
30
+ def json
31
+ @json ||= JSON.parse(body)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module HttpClient
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "http_client/version"
4
+ require_relative "http_client/response"
5
+ require_relative "http_client/connection"
6
+ require_relative "http_client/client"
7
+
8
+ module Philiprehberger
9
+ module HttpClient
10
+ # Convenience constructor.
11
+ #
12
+ # @param options [Hash] Forwarded to {Client#initialize}
13
+ # @return [Client]
14
+ def self.new(**options)
15
+ Client.new(**options)
16
+ end
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: philiprehberger-http_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Philip Rehberger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A zero-dependency HTTP client built on Ruby's net/http with automatic
14
+ retries, request/response interceptors, and a clean API for JSON services.
15
+ email:
16
+ - me@philiprehberger.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE
23
+ - README.md
24
+ - lib/philiprehberger/http_client.rb
25
+ - lib/philiprehberger/http_client/client.rb
26
+ - lib/philiprehberger/http_client/connection.rb
27
+ - lib/philiprehberger/http_client/response.rb
28
+ - lib/philiprehberger/http_client/version.rb
29
+ homepage: https://github.com/philiprehberger/rb-http-client
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ homepage_uri: https://github.com/philiprehberger/rb-http-client
34
+ source_code_uri: https://github.com/philiprehberger/rb-http-client
35
+ changelog_uri: https://github.com/philiprehberger/rb-http-client/blob/main/CHANGELOG.md
36
+ rubygems_mfa_required: 'true'
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '3.1'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubygems_version: 3.5.22
53
+ signing_key:
54
+ specification_version: 4
55
+ summary: Lightweight HTTP client wrapper with retries and interceptors
56
+ test_files: []