Skip to content

Commit

Permalink
Support HTTP 1.0 (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
quinnj authored Aug 15, 2022
1 parent 1b5f835 commit 684447a
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
fail-fast: false
matrix:
version:
- '1.0'
- '1.6'
- '1'
- 'nightly'
steps:
Expand Down
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6"

[compat]
BSON = "0.2, 0.3"
HTTP = "0.9"
HTTP = "1"
JLD2 = "0.3, 0.4"
JLSO = "2"
JSON = "0.21"
YAML = "0.4.6"
julia = "1"
julia = "1.6"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Expand Down
17 changes: 6 additions & 11 deletions src/BrokenRecord.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ export playback
using Base.Threads: nthreads, threadid

using BSON: BSON
using HTTP: HTTP, Header, Layer, Response, URI, body_was_streamed, header, insert_default!,
mkheaders, nobody, remove_default!, request, request_uri, stack, top_layer
using HTTP: HTTP, Header, Response, URI, header, mkheaders
using JLD2: JLD2
using JLSO: JLSO
using JSON: JSON
Expand Down Expand Up @@ -79,19 +78,15 @@ function playback(
path = joinpath(DEFAULTS[:path], replace(path, isspace => "_"))
storage, path = get_storage(path, DEFAULTS[:extension])

before_layer, custom_layer = if isfile(path)
top_layer(stack()), PlaybackLayer
else
Union{}, RecordingLayer
end
T, layer = isfile(path) ? (PlaybackLayer, playbacklayer) : (RecordingLayer, recordinglayer)

before(custom_layer, storage, path)
insert_default!(before_layer, custom_layer)
before(T, storage, path)
HTTP.pushlayer!(layer)
return try
f()
finally
remove_default!(before_layer, custom_layer)
after(custom_layer, storage, path)
HTTP.poplayer!()
after(T, storage, path)
end
end

Expand Down
40 changes: 19 additions & 21 deletions src/playback.jl
Original file line number Diff line number Diff line change
@@ -1,41 +1,37 @@
abstract type PlaybackLayer{Next <: Layer} <: Layer{Next} end
struct PlaybackLayer end

const NoQuery = Dict{SubString{String}, SubString{String}}

function before(::Type{<:PlaybackLayer}, storage, path)
function before(::Type{PlaybackLayer}, storage, path)
data = load(storage, path)
state = get_state()
append!(state.responses, data[:responses])
end

function HTTP.request(::Type{<:PlaybackLayer}, m, u, h=Header[], b=nobody; kwargs...)
method = string(m)
uri = request_uri(u, get(kwargs, :query, nothing))
headers = mkheaders(get(kwargs, :headers, h))
body = get(kwargs, :body, b)
state = get_state()
isempty(state.responses) && error("No responses remaining in the data file")
response = popfirst!(state.responses)
request = response.request
check_body(request, body)
check_method(request, method)
check_headers(request, headers; ignore=state.ignore_headers)
check_uri(request, uri; ignore=state.ignore_query)
return response
function playbacklayer(handler)
function playback(req; kw...)
state = get_state()
isempty(state.responses) && error("No responses remaining in the data file")
response = popfirst!(state.responses)
request = response.request
check_body(request, req.body)
check_method(request, req.method)
check_headers(request, req.headers; ignore=state.ignore_headers)
check_uri(request, req.url; ignore=state.ignore_query)
return response
end
end

function after(::Type{<:PlaybackLayer}, storage, path)
function after(::Type{PlaybackLayer}, storage, path)
state = get_state()
isempty(state.responses) || error("Found unused responses")
end

parse_query(uri) = isempty(uri.query) ? NoQuery() : Dict(split.(split(uri.query, '&'), '='))

function check_body(request, body)
if request.body == body_was_streamed
if request.body == b"[Message Body was streamed]"
@warn "Can't verify streamed request body"
else
request.body == Vector{UInt8}(body) || error("Request body does not match")
request.body == body || error("Request body does not match")
end
end

Expand All @@ -49,6 +45,8 @@ function check_headers(request, headers; ignore)
issubset(observed, request.headers) || error("Request headers do not match")
end

parse_query(uri) = isempty(uri.query) ? NoQuery() : Dict(split.(split(uri.query, '&'), '='))

function check_uri(request, uri; ignore)
# Check host.
host = header(request, "Host")
Expand Down
17 changes: 10 additions & 7 deletions src/recording.jl
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
abstract type RecordingLayer{Next <: Layer} <: Layer{Next} end
struct RecordingLayer end

before(::Type{<:RecordingLayer}, storage, path) = nothing
before(::Type{RecordingLayer}, storage, path) = nothing

function HTTP.request(::Type{RecordingLayer{Next}}, resp) where Next
state = get_state()
push!(state.responses, deepcopy(resp))
return request(Next, resp)
function recordinglayer(handler)
function record(req; kw...)
resp = handler(req; kw...)
state = get_state()
push!(state.responses, deepcopy(resp))
return resp
end
end

function after(::Type{<:RecordingLayer}, storage, path)
function after(::Type{RecordingLayer}, storage, path)
state = get_state()
for resp in state.responses
filter!(drop_keys(state.ignore_headers), resp.request.headers)
Expand Down
14 changes: 11 additions & 3 deletions src/storage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function resp_to_dict(resp)
"headers" => req.headers,
"target" => req.target,
"body" => repr_body(req.body),
"txcount" => req.txcount,
"txcount" => 0,
"version" => string(req.version),
),
)
Expand All @@ -52,12 +52,20 @@ function dict_to_resp(dict)
resp.request.body = Vector{UInt8}(dict["request"]["body"])
resp.request.headers = dicts_to_pairs(dict["request"]["headers"])
resp.request.target = dict["request"]["target"]
resp.request.txcount = dict["request"]["txcount"]
resp.request.version = VersionNumber(dict["request"]["version"])
return resp
end

repr_body(body) = isvalid(String, body) ? String(body) : resp.body
function repr_body(body)
if !HTTP.isbytes(body)
return "[Message Body was streamed]"
else
buf = IOBuffer()
write(buf, body)
return String(take!(buf))
end
end

dicts_to_pairs(dicts) = map(d -> first(pairs(d)), dicts)
write_json(path, data) = open(io -> JSON.print(io, data, 2), path, "w")

Expand Down
5 changes: 3 additions & 2 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Test: @test, @testset, @test_logs, @test_throws

using HTTP: HTTP, Form, Request, Response
using HTTP: HTTP, Form, Request, Response, URIs
using JSON: JSON

using BrokenRecord: BrokenRecord, FORMAT, configure!, playback
Expand Down Expand Up @@ -104,7 +104,8 @@ const url = "https://httpbin.org"
HTTP.post("$url/post"; body=Form(Dict(:file => f)))
end
end
playback(path) do
# ignore Content-Type/Content-Length because for Form, a random boundary is generated.
playback(path, ignore_headers=["Content-Type", "Content-Length"]) do
open(@__FILE__) do f
@test_logs (:warn, r"streamed") HTTP.post("$url/post"; body=Form(Dict(:file => f)))
end
Expand Down

2 comments on commit 684447a

@christopher-dG
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error while trying to register: "Tag with name v0.1.8 already exists and points to a different commit"

Please sign in to comment.