async-http-capture 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 +7 -0
- data/context/getting-started.md +186 -0
- data/context/index.yaml +12 -0
- data/design.md +771 -0
- data/lib/async/http/capture/cassette.rb +69 -0
- data/lib/async/http/capture/cassette_store.rb +47 -0
- data/lib/async/http/capture/console_store.rb +61 -0
- data/lib/async/http/capture/interaction.rb +269 -0
- data/lib/async/http/capture/interaction_tracker.rb +144 -0
- data/lib/async/http/capture/middleware.rb +117 -0
- data/lib/async/http/capture/version.rb +12 -0
- data/lib/async/http/capture.rb +22 -0
- data/license.md +21 -0
- data/readme.md +133 -0
- data/releases.md +5 -0
- metadata +67 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
|
+
|
6
|
+
require "protocol/http/middleware"
|
7
|
+
require "protocol/http/body/rewindable"
|
8
|
+
require "protocol/http/body/completable"
|
9
|
+
|
10
|
+
require_relative "interaction_tracker"
|
11
|
+
|
12
|
+
module Async
|
13
|
+
module HTTP
|
14
|
+
module Capture
|
15
|
+
# Protocol::HTTP::Middleware for recording complete HTTP interactions.
|
16
|
+
#
|
17
|
+
# This middleware captures both HTTP requests and responses, waiting for both
|
18
|
+
# to be fully processed before recording the complete interaction.
|
19
|
+
class Middleware < Protocol::HTTP::Middleware
|
20
|
+
# Initialize the recording middleware.
|
21
|
+
# @parameter app [Protocol::HTTP::Middleware] The next middleware in the chain.
|
22
|
+
# @parameter store [Object] An object that responds to #call(interaction) to handle recorded interactions.
|
23
|
+
def initialize(app, store:)
|
24
|
+
super(app)
|
25
|
+
@store = store
|
26
|
+
end
|
27
|
+
|
28
|
+
# Process an HTTP request, capturing both request and response.
|
29
|
+
# @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
|
30
|
+
# @returns [Protocol::HTTP::Response] The response from the next middleware.
|
31
|
+
def call(request)
|
32
|
+
# Check if we should capture this request:
|
33
|
+
unless capture?(request)
|
34
|
+
return super(request)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create completion tracker for this interaction:
|
38
|
+
tracker = create_interaction_tracker(request)
|
39
|
+
|
40
|
+
# Capture request body with completion tracking:
|
41
|
+
captured_request = capture_request_with_completion(request, tracker)
|
42
|
+
|
43
|
+
# Get response from downstream middleware/app:
|
44
|
+
response = super(captured_request)
|
45
|
+
|
46
|
+
# Capture response body with completion tracking:
|
47
|
+
capture_response_with_completion(captured_request, response, tracker)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Determine whether to capture the given request.
|
51
|
+
# Override this method in subclasses to implement custom filtering logic.
|
52
|
+
# @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
|
53
|
+
# @returns [Boolean] True if the request should be captured, false otherwise.
|
54
|
+
def capture?(request)
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Create a completion tracker for a single interaction.
|
61
|
+
# @parameter request [Protocol::HTTP::Request] The original request.
|
62
|
+
# @returns [InteractionTracker] A new tracker instance.
|
63
|
+
def create_interaction_tracker(request)
|
64
|
+
InteractionTracker.new(@store, request)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Capture the request body with completion tracking.
|
68
|
+
# @parameter request [Protocol::HTTP::Request] The original request.
|
69
|
+
# @parameter tracker [InteractionTracker] The completion tracker.
|
70
|
+
# @returns [Protocol::HTTP::Request] A request with rewindable body.
|
71
|
+
def capture_request_with_completion(request, tracker)
|
72
|
+
return tracker.mark_request_ready(request) unless request.body && !request.body.empty?
|
73
|
+
|
74
|
+
# Make the request body rewindable:
|
75
|
+
rewindable_body = ::Protocol::HTTP::Body::Rewindable.wrap(request)
|
76
|
+
|
77
|
+
# Wrap with completion callback:
|
78
|
+
::Protocol::HTTP::Body::Completable.wrap(request) do |error|
|
79
|
+
if error
|
80
|
+
tracker.request_completed(error: error)
|
81
|
+
else
|
82
|
+
tracker.request_completed(body: rewindable_body.buffered)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
return request
|
87
|
+
end
|
88
|
+
|
89
|
+
# Capture the response body with completion tracking.
|
90
|
+
# @parameter request [Protocol::HTTP::Request] The captured request.
|
91
|
+
# @parameter response [Protocol::HTTP::Response] The response to capture.
|
92
|
+
# @parameter tracker [InteractionTracker] The completion tracker.
|
93
|
+
# @returns [Protocol::HTTP::Response] The wrapped response.
|
94
|
+
def capture_response_with_completion(request, response, tracker)
|
95
|
+
# Set the response on the tracker:
|
96
|
+
tracker.set_response(response)
|
97
|
+
|
98
|
+
return tracker.mark_response_ready(response) unless response.body && !response.body.empty?
|
99
|
+
|
100
|
+
# Make the response body rewindable:
|
101
|
+
rewindable_body = ::Protocol::HTTP::Body::Rewindable.wrap(response)
|
102
|
+
|
103
|
+
# Wrap with completion callback:
|
104
|
+
::Protocol::HTTP::Body::Completable.wrap(response) do |error|
|
105
|
+
if error
|
106
|
+
tracker.response_completed(error: error)
|
107
|
+
else
|
108
|
+
tracker.response_completed(body: rewindable_body.buffered)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
return response
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative "capture/version"
|
7
|
+
require_relative "capture/interaction"
|
8
|
+
require_relative "capture/cassette"
|
9
|
+
require_relative "capture/cassette_store"
|
10
|
+
require_relative "capture/console_store"
|
11
|
+
require_relative "capture/interaction_tracker"
|
12
|
+
require_relative "capture/middleware"
|
13
|
+
|
14
|
+
# @namespace
|
15
|
+
module Async
|
16
|
+
# @namespace
|
17
|
+
module HTTP
|
18
|
+
# @namespace
|
19
|
+
module Capture
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/license.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# MIT License
|
2
|
+
|
3
|
+
Copyright, 2025, by Samuel Williams.
|
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,133 @@
|
|
1
|
+
# Async::HTTP::Capture
|
2
|
+
|
3
|
+
A Ruby gem for recording and replaying HTTP requests using `Protocol::HTTP`. Features content-addressed storage, parallel-safe recording, and flexible store backends.
|
4
|
+
|
5
|
+
[](https://github.com/socketry/async-http-capture/actions?workflow=Test)
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
- **Pure Protocol::HTTP**: Works directly with Protocol::HTTP objects, no lossy conversions
|
10
|
+
- **Content-Addressed Storage**: Each interaction saved as separate JSON file with content hash
|
11
|
+
- **Parallel-Safe**: Multiple processes can record simultaneously without conflicts
|
12
|
+
- **Flexible Stores**: Pluggable storage backends (files, console logging, etc.)
|
13
|
+
- **Complete Headers**: Full round-trip serialization including `fields` and `tail`
|
14
|
+
- **Error Handling**: Captures network errors and connection issues
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
Please see the [project documentation](https://socketry.github.io/async-http-capture/) for more details.
|
19
|
+
|
20
|
+
- [Getting Started](https://socketry.github.io/async-http-capture/guides/getting-started/index) - This guide explains how to get started with `async-http-capture`, a Ruby gem for recording and replaying HTTP requests using Protocol::HTTP.
|
21
|
+
|
22
|
+
### Basic Recording to Files
|
23
|
+
|
24
|
+
``` ruby
|
25
|
+
require "async/http/capture"
|
26
|
+
|
27
|
+
# Create a store that saves to content-addressed files:
|
28
|
+
store = Async::HTTP::Capture::CassetteStore.new("interactions")
|
29
|
+
|
30
|
+
# Create middleware:
|
31
|
+
app = ->(request) { Protocol::HTTP::Response[200, {}, ["OK"]] }
|
32
|
+
middleware = Async::HTTP::Capture::Middleware.new(app, store: store)
|
33
|
+
|
34
|
+
# Record interactions:
|
35
|
+
request = Protocol::HTTP::Request["GET", "/users"]
|
36
|
+
response = middleware.call(request)
|
37
|
+
```
|
38
|
+
|
39
|
+
### Recording to Console Log
|
40
|
+
|
41
|
+
``` ruby
|
42
|
+
# Create a console store for debugging:
|
43
|
+
console_store = Async::HTTP::Capture::ConsoleStore.new
|
44
|
+
middleware = Async::HTTP::Capture::Middleware.new(app, store: console_store)
|
45
|
+
|
46
|
+
# This will log interactions to console:
|
47
|
+
middleware.call(request)
|
48
|
+
# Output: "Recorded: GET /users"
|
49
|
+
```
|
50
|
+
|
51
|
+
### Loading and Replaying
|
52
|
+
|
53
|
+
``` ruby
|
54
|
+
# Load recorded interactions:
|
55
|
+
cassette = Async::HTTP::Capture::Cassette.load("interactions")
|
56
|
+
|
57
|
+
# Replay them:
|
58
|
+
cassette.each do |interaction|
|
59
|
+
request = interaction.request # Lazy construction
|
60
|
+
response = app.call(request) # Send to your app
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
### Recording with Responses
|
65
|
+
|
66
|
+
``` ruby
|
67
|
+
# Record both requests and responses:
|
68
|
+
middleware = Async::HTTP::Capture::Middleware.new(
|
69
|
+
app,
|
70
|
+
store: store,
|
71
|
+
record_response: true
|
72
|
+
)
|
73
|
+
|
74
|
+
response = middleware.call(request)
|
75
|
+
# Both request and response are now recorded
|
76
|
+
```
|
77
|
+
|
78
|
+
## Architecture
|
79
|
+
|
80
|
+
Middleware -> Store.call(interaction) -> [CassetteStore | ConsoleStore | ...]
|
81
|
+
|
82
|
+
- **Middleware**: Pure capture logic, creates Interaction objects with Protocol::HTTP data
|
83
|
+
- **Store Interface**: Generic `call(interaction)` method for pluggable backends
|
84
|
+
- **Stores**: Handle serialization, filtering, persistence, or logging
|
85
|
+
- **Interaction**: Simple data container with lazy Protocol::HTTP object construction
|
86
|
+
|
87
|
+
## Content-Addressed Storage
|
88
|
+
|
89
|
+
Each interaction is saved to a file named by its content hash:
|
90
|
+
|
91
|
+
interactions/
|
92
|
+
├── a1b2c3d4e5f67890.json # GET /users
|
93
|
+
├── f67890a1b2c3d4e5.json # POST /orders
|
94
|
+
└── 1234567890abcdef.json # GET /health
|
95
|
+
|
96
|
+
Benefits:
|
97
|
+
|
98
|
+
- **Automatic de-duplication**: Identical interactions → same filename
|
99
|
+
- **Parallel-safe**: Multiple processes can write without conflicts
|
100
|
+
- **Content integrity**: Hash verifies file contents
|
101
|
+
- **Git-friendly**: Stable filenames for version control
|
102
|
+
|
103
|
+
## Store Implementations
|
104
|
+
|
105
|
+
### CassetteStore
|
106
|
+
|
107
|
+
Saves interactions to content-addressed JSON files in a directory.
|
108
|
+
|
109
|
+
### ConsoleStore
|
110
|
+
|
111
|
+
Logs interactions via the Console gem with different levels based on success/failure.
|
112
|
+
|
113
|
+
### Custom Stores
|
114
|
+
|
115
|
+
Implement the `Store` interface:
|
116
|
+
|
117
|
+
``` ruby
|
118
|
+
class MyStore
|
119
|
+
include Async::HTTP::Capture::Store
|
120
|
+
|
121
|
+
def call(interaction)
|
122
|
+
# Handle the interaction as needed
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
## Testing
|
128
|
+
|
129
|
+
``` bash
|
130
|
+
bundle exec sus
|
131
|
+
```
|
132
|
+
|
133
|
+
The gem includes comprehensive tests using the Sus testing framework with 41 tests and 101 assertions.
|
data/releases.md
ADDED
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: async-http-capture
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Samuel Williams
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: async-http
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0.90'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0.90'
|
26
|
+
executables: []
|
27
|
+
extensions: []
|
28
|
+
extra_rdoc_files: []
|
29
|
+
files:
|
30
|
+
- context/getting-started.md
|
31
|
+
- context/index.yaml
|
32
|
+
- design.md
|
33
|
+
- lib/async/http/capture.rb
|
34
|
+
- lib/async/http/capture/cassette.rb
|
35
|
+
- lib/async/http/capture/cassette_store.rb
|
36
|
+
- lib/async/http/capture/console_store.rb
|
37
|
+
- lib/async/http/capture/interaction.rb
|
38
|
+
- lib/async/http/capture/interaction_tracker.rb
|
39
|
+
- lib/async/http/capture/middleware.rb
|
40
|
+
- lib/async/http/capture/version.rb
|
41
|
+
- license.md
|
42
|
+
- readme.md
|
43
|
+
- releases.md
|
44
|
+
homepage: https://github.com/socketry/async-http-capture
|
45
|
+
licenses:
|
46
|
+
- MIT
|
47
|
+
metadata:
|
48
|
+
documentation_uri: https://socketry.github.io/async-http-capture/
|
49
|
+
source_code_uri: https://github.com/socketry/async-http-capture.git
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '3.2'
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubygems_version: 3.6.9
|
65
|
+
specification_version: 4
|
66
|
+
summary: A HTTP request and response capture.
|
67
|
+
test_files: []
|