Skip to content

Commit 26f6390

Browse files
Add example for recording and replaying.
1 parent 5955f89 commit 26f6390

File tree

13 files changed

+704
-2
lines changed

13 files changed

+704
-2
lines changed

async-http-capture.gemspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
require_relative "lib/async/http/capture/version"
44

55
Gem::Specification.new do |spec|
6-
spec.name = "async-http"
6+
spec.name = "async-http-capture"
77
spec.version = Async::HTTP::Capture::VERSION
88

99
spec.summary = "A HTTP request and response capture."
1010
spec.authors = ["Samuel Williams"]
1111
spec.license = "MIT"
1212

13-
spec.homepage = "https://github.com/socketry/async-http"
13+
spec.homepage = "https://github.com/socketry/async-http-capture"
1414

1515
spec.metadata = {
1616
"documentation_uri" => "https://socketry.github.io/async-http-capture/",

examples/recorder/config.ru

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Simple Rack application demonstrating async-http-capture recording
5+
# This application provides several endpoints that generate different types of responses
6+
7+
require 'json'
8+
require 'time'
9+
10+
# Simple hello world application with multiple endpoints
11+
app = lambda do |env|
12+
request = Rack::Request.new(env)
13+
14+
case request.path_info
15+
when '/'
16+
# Simple homepage
17+
body = <<~HTML
18+
<html>
19+
<head><title>Recorder Example</title></head>
20+
<body>
21+
<h1>HTTP Capture Recording Example</h1>
22+
<p>This application demonstrates async-http-capture recording.</p>
23+
<ul>
24+
<li><a href="/hello">Simple Hello</a></li>
25+
<li><a href="/json">JSON Response</a></li>
26+
<li><a href="/headers">Show Request Headers</a></li>
27+
<li><a href="/post" onclick="postExample()">POST Example</a></li>
28+
</ul>
29+
<script>
30+
function postExample() {
31+
fetch('/post', {
32+
method: 'POST',
33+
headers: {'Content-Type': 'application/json'},
34+
body: JSON.stringify({message: 'Hello from browser!'})
35+
}).then(r => r.text()).then(alert);
36+
}
37+
</script>
38+
</body>
39+
</html>
40+
HTML
41+
42+
[200, {'Content-Type' => 'text/html'}, [body]]
43+
44+
when '/hello'
45+
# Simple text response
46+
[200, {'Content-Type' => 'text/plain'}, ["Hello, World! #{Time.now}"]]
47+
48+
when '/json'
49+
# JSON response
50+
data = {
51+
message: "Hello from JSON API",
52+
timestamp: Time.now.iso8601,
53+
random: rand(1000)
54+
}
55+
[200, {'Content-Type' => 'application/json'}, [JSON.generate(data)]]
56+
57+
when '/headers'
58+
# Show request headers
59+
headers_info = env.select { |k, _| k.start_with?('HTTP_') }
60+
.transform_keys { |k| k.sub('HTTP_', '').split('_').map(&:capitalize).join('-') }
61+
62+
response = {
63+
method: request.request_method,
64+
path: request.path_info,
65+
query: request.query_string,
66+
headers: headers_info
67+
}
68+
69+
[200, {'Content-Type' => 'application/json'}, [JSON.pretty_generate(response)]]
70+
71+
when '/post'
72+
# Handle POST requests
73+
if request.request_method == 'POST'
74+
# Read request body
75+
body = request.body.read
76+
77+
response = {
78+
message: "Received POST request",
79+
content_type: request.content_type,
80+
body_length: body.length,
81+
body_preview: body[0..100], # First 100 chars
82+
timestamp: Time.now.iso8601
83+
}
84+
85+
[200, {'Content-Type' => 'application/json'}, [JSON.pretty_generate(response)]]
86+
else
87+
[405, {'Content-Type' => 'text/plain'}, ['Method Not Allowed - Use POST']]
88+
end
89+
90+
when '/error'
91+
# Generate an error for testing error recording
92+
raise "Intentional error for testing"
93+
94+
when '/slow'
95+
# Simulate a slow response
96+
sleep(1)
97+
[200, {'Content-Type' => 'text/plain'}, ["Slow response after 1 second delay"]]
98+
99+
else
100+
# 404 Not Found
101+
[404, {'Content-Type' => 'text/plain'}, ['Not Found']]
102+
end
103+
end
104+
105+
# If running with plain rackup (not Falcon), we can add the middleware here
106+
# But when using Falcon with falcon.rb, the middleware is configured there
107+
if ENV['CAPTURE_ENABLED'] == 'true'
108+
require 'async/http/capture'
109+
110+
# Create stores for recording
111+
store = Async::HTTP::Capture::CassetteStore.new("recordings")
112+
console_store = Async::HTTP::Capture::ConsoleStore.new
113+
114+
combined_store = proc do |interaction|
115+
store.call(interaction)
116+
console_store.call(interaction)
117+
end
118+
119+
# Wrap the app with capture middleware
120+
app = Async::HTTP::Capture::Middleware.new(
121+
app,
122+
store: combined_store,
123+
record_response: true
124+
)
125+
end
126+
127+
run app

examples/recorder/falcon.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env falcon-host
2+
# frozen_string_literal: true
3+
4+
# Load the capture library
5+
require "async/http/capture"
6+
require "falcon/environment/application"
7+
8+
# Configure the host and port for the server
9+
service "recorder-example" do
10+
include Falcon::Environment::Application
11+
12+
# Bind to localhost on port 9292
13+
endpoint Async::HTTP::Endpoint.parse(
14+
"http://localhost:9292"
15+
)
16+
17+
# This is where we set up the recording middleware
18+
# The middleware will automatically capture all requests and responses
19+
middleware do
20+
# Create a cassette store for recording interactions
21+
store = Async::HTTP::Capture::CassetteStore.new("recordings")
22+
23+
# Also create a console store for debugging
24+
console_store = Async::HTTP::Capture::ConsoleStore.new
25+
26+
# You can chain multiple stores if needed
27+
combined_store = proc do |interaction|
28+
store.call(interaction)
29+
console_store.call(interaction)
30+
end
31+
32+
application = Protocol::HTTP::Middleware.build do
33+
run ::Protocol::HTTP::Middleware::HelloWorld
34+
end
35+
36+
middleware = Async::HTTP::Capture::Middleware.new(
37+
application,
38+
store: combined_store
39+
)
40+
41+
# Replay interactions:
42+
cassette = store.cassette
43+
cassette.each do |interaction|
44+
puts "Replaying interaction: #{interaction}"
45+
response = application.call(interaction.request)
46+
response.finish
47+
end
48+
49+
middleware
50+
end
51+
end

examples/recorder/gems.locked

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
PATH
2+
remote: ../..
3+
specs:
4+
async-http-capture (0.0.0)
5+
async-http (~> 0.90)
6+
7+
GEM
8+
remote: https://rubygems.org/
9+
specs:
10+
async (2.27.3)
11+
console (~> 1.29)
12+
fiber-annotation
13+
io-event (~> 1.11)
14+
metrics (~> 0.12)
15+
traces (~> 0.15)
16+
async-container (0.25.0)
17+
async (~> 2.22)
18+
async-container-supervisor (0.5.2)
19+
async-container (~> 0.22)
20+
async-service
21+
io-endpoint
22+
io-stream
23+
memory-leak (~> 0.5)
24+
async-http (0.91.0)
25+
async (>= 2.10.2)
26+
async-pool (~> 0.11)
27+
io-endpoint (~> 0.14)
28+
io-stream (~> 0.6)
29+
metrics (~> 0.12)
30+
protocol-http (~> 0.49)
31+
protocol-http1 (~> 0.30)
32+
protocol-http2 (~> 0.22)
33+
traces (~> 0.10)
34+
async-http-cache (0.4.5)
35+
async-http (~> 0.56)
36+
async-pool (0.11.0)
37+
async (>= 2.0)
38+
async-service (0.14.1)
39+
async
40+
async-container (~> 0.16)
41+
console (1.33.0)
42+
fiber-annotation
43+
fiber-local (~> 1.1)
44+
json
45+
falcon (0.52.3)
46+
async
47+
async-container (~> 0.20)
48+
async-container-supervisor (~> 0.5.0)
49+
async-http (~> 0.75)
50+
async-http-cache (~> 0.4)
51+
async-service (~> 0.10)
52+
bundler
53+
localhost (~> 1.1)
54+
openssl (~> 3.0)
55+
protocol-http (~> 0.31)
56+
protocol-rack (~> 0.7)
57+
samovar (~> 2.3)
58+
fiber-annotation (0.2.0)
59+
fiber-local (1.1.0)
60+
fiber-storage
61+
fiber-storage (1.0.1)
62+
io-endpoint (0.15.2)
63+
io-event (1.12.1)
64+
io-stream (0.10.0)
65+
json (2.13.2)
66+
localhost (1.6.0)
67+
mapping (1.1.3)
68+
memory-leak (0.5.2)
69+
metrics (0.13.0)
70+
openssl (3.3.0)
71+
protocol-hpack (1.5.1)
72+
protocol-http (0.52.0)
73+
protocol-http1 (0.34.1)
74+
protocol-http (~> 0.22)
75+
protocol-http2 (0.22.1)
76+
protocol-hpack (~> 1.4)
77+
protocol-http (~> 0.47)
78+
protocol-rack (0.16.0)
79+
io-stream (>= 0.10)
80+
protocol-http (~> 0.43)
81+
rack (>= 1.0)
82+
rack (3.2.0)
83+
samovar (2.3.0)
84+
console (~> 1.0)
85+
mapping (~> 1.0)
86+
traces (0.17.0)
87+
88+
PLATFORMS
89+
arm64-darwin-24
90+
ruby
91+
92+
DEPENDENCIES
93+
async-http-capture!
94+
falcon
95+
96+
BUNDLED WITH
97+
2.7.0

examples/recorder/gems.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
source "https://rubygems.org"
2+
3+
gem "falcon"
4+
gem "async-http-capture", path: "../../"

examples/recorder/readme.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# HTTP Capture Recording Example
2+
3+
This example demonstrates how to use `async-http-capture` with Falcon to record HTTP interactions.
4+
5+
## Files
6+
7+
- `falcon.rb` - Falcon server configuration with recording middleware and simple HelloWorld app
8+
- `config.ru` - Alternative Rack application with multiple endpoints (for use with rackup)
9+
10+
## Usage
11+
12+
1. **Run Falcon**:
13+
```bash
14+
bundle exec ./falcon.rb
15+
```
16+
17+
2. **Make some requests**:
18+
```bash
19+
curl http://localhost:9292/
20+
curl http://localhost:9292/test
21+
curl -X POST http://localhost:9292/api -d '{"test": true}'
22+
```
23+
24+
3. **Check the results**:
25+
- **Console output**: See real-time JSON logging of each interaction
26+
- **Recordings directory**: Check `recordings/` for saved JSON files
27+
28+
Each unique request gets saved as a content-addressed JSON file for parallel-safe recording.

0 commit comments

Comments
 (0)