Skip to content

Commit 45e6e7d

Browse files
authored
http: add support for stream connections, and custom .on_redirect, .on_progress, .on_finish callbacks to http.fetch() (#19184)
1 parent d60c817 commit 45e6e7d

5 files changed

Lines changed: 120 additions & 8 deletions

File tree

vlib/net/http/backend_nix.c.v

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,17 @@ fn (req &Request) ssl_do(port int, method Method, host_name string, path string)
3737
eprintln('-'.repeat(20))
3838
}
3939
unsafe { content.write_ptr(bp, len) }
40+
if req.on_progress != unsafe { nil } {
41+
req.on_progress(req, content[content.len - len..], u64(content.len))!
42+
}
4043
}
4144
ssl_conn.shutdown()!
4245
response_text := content.str()
4346
$if trace_http_response ? {
4447
eprintln('< ${response_text}')
4548
}
49+
if req.on_finish != unsafe { nil } {
50+
req.on_finish(req, u64(response_text.len))!
51+
}
4652
return parse_response(response_text)
4753
}

vlib/net/http/backend_windows.c.v

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,14 @@ fn (req &Request) ssl_do(port int, method Method, host_name string, path string)
2525
length := C.request(&ctx, port, addr.to_wide(), sdata.str, &buff)
2626
C.vschannel_cleanup(&ctx)
2727
response_text := unsafe { buff.vstring_with_len(length) }
28+
if req.on_progress != unsafe { nil } {
29+
req.on_progress(req, unsafe { buff.vbytes(length) }, u64(length))!
30+
}
2831
$if trace_http_response ? {
2932
eprintln('< ${response_text}')
3033
}
34+
if req.on_finish != unsafe { nil } {
35+
req.on_finish(req, u64(response_text.len))!
36+
}
3137
return parse_response(response_text)
3238
}

vlib/net/http/http.v

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ pub mut:
2020
data string
2121
params map[string]string
2222
cookies map[string]string
23-
user_agent string = 'v.http'
23+
user_agent string = 'v.http'
24+
user_ptr voidptr = unsafe { nil }
2425
verbose bool
2526
//
2627
validate bool // set this to true, if you want to stop requests, when their certificates are found to be invalid
@@ -29,6 +30,10 @@ pub mut:
2930
cert_key string // the path to a key.pem file, containing private keys for the client certificate(s)
3031
in_memory_verification bool // if true, verify, cert, and cert_key are read from memory, not from a file
3132
allow_redirect bool = true // whether to allow redirect
33+
// callbacks to allow custom reporting code to run, while the request is running
34+
on_redirect RequestRedirectFn = unsafe { nil }
35+
on_progress RequestProgressFn = unsafe { nil }
36+
on_finish RequestFinishFn = unsafe { nil }
3237
}
3338

3439
// new_request creates a new Request given the request `method`, `url_`, and
@@ -149,14 +154,17 @@ pub fn fetch(config FetchConfig) !Response {
149154
header: config.header
150155
cookies: config.cookies
151156
user_agent: config.user_agent
152-
user_ptr: 0
157+
user_ptr: config.user_ptr
153158
verbose: config.verbose
154159
validate: config.validate
155160
verify: config.verify
156161
cert: config.cert
157162
cert_key: config.cert_key
158163
in_memory_verification: config.in_memory_verification
159164
allow_redirect: config.allow_redirect
165+
on_progress: config.on_progress
166+
on_redirect: config.on_redirect
167+
on_finish: config.on_finish
160168
}
161169
res := req.do()!
162170
return res

vlib/net/http/request.v

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import rand
1010
import strings
1111
import time
1212

13+
pub type RequestRedirectFn = fn (request &Request, nredirects int, new_url string) !
14+
15+
pub type RequestProgressFn = fn (request &Request, chunk []u8, read_so_far u64) !
16+
17+
pub type RequestFinishFn = fn (request &Request, final_size u64) !
18+
1319
// Request holds information about an HTTP request (either received by
1420
// a server or to be sent by a client)
1521
pub struct Request {
@@ -35,6 +41,10 @@ pub mut:
3541
cert_key string
3642
in_memory_verification bool // if true, verify, cert, and cert_key are read from memory, not from a file
3743
allow_redirect bool = true // whether to allow redirect
44+
// callbacks to allow custom reporting code to run, while the request is running
45+
on_redirect RequestRedirectFn = unsafe { nil }
46+
on_progress RequestProgressFn = unsafe { nil }
47+
on_finish RequestFinishFn = unsafe { nil }
3848
}
3949

4050
fn (mut req Request) free() {
@@ -58,9 +68,9 @@ pub fn (req &Request) do() !Response {
5868
mut url := urllib.parse(req.url) or { return error('http.Request.do: invalid url ${req.url}') }
5969
mut rurl := url
6070
mut resp := Response{}
61-
mut no_redirects := 0
71+
mut nredirects := 0
6272
for {
63-
if no_redirects == max_redirects {
73+
if nredirects == max_redirects {
6474
return error('http.request.do: maximum number of redirects reached (${max_redirects})')
6575
}
6676
qresp := req.method_and_url_to_response(req.method, rurl)!
@@ -80,11 +90,14 @@ pub fn (req &Request) do() !Response {
8090
}
8191
redirect_url = url.str()
8292
}
93+
if req.on_redirect != unsafe { nil } {
94+
req.on_redirect(req, nredirects, redirect_url)!
95+
}
8396
qrurl := urllib.parse(redirect_url) or {
8497
return error('http.request.do: invalid URL in redirect "${redirect_url}"')
8598
}
8699
rurl = qrurl
87-
no_redirects++
100+
nredirects++
88101
}
89102
return resp
90103
}
@@ -164,15 +177,35 @@ fn (req &Request) http_do(host string, method Method, path string) !Response {
164177
$if trace_http_request ? {
165178
eprintln('> ${s}')
166179
}
167-
mut bytes := io.read_all(reader: client)!
180+
mut bytes := req.read_all_from_client_connection(client)!
168181
client.close()!
169182
response_text := bytes.bytestr()
170183
$if trace_http_response ? {
171184
eprintln('< ${response_text}')
172185
}
186+
if req.on_finish != unsafe { nil } {
187+
req.on_finish(req, u64(response_text.len))!
188+
}
173189
return parse_response(response_text)
174190
}
175191

192+
fn (req &Request) read_all_from_client_connection(r &net.TcpConn) ![]u8 {
193+
mut read := i64(0)
194+
mut b := []u8{len: 32768}
195+
for {
196+
old_read := read
197+
new_read := r.read(mut b[read..]) or { break }
198+
read += new_read
199+
if req.on_progress != unsafe { nil } {
200+
req.on_progress(req, b[old_read..read], u64(read))!
201+
}
202+
for b.len <= read {
203+
unsafe { b.grow_len(4096) }
204+
}
205+
}
206+
return b[..read]
207+
}
208+
176209
// referer returns 'Referer' header value of the given request
177210
pub fn (req &Request) referer() string {
178211
return req.header.get(.referer) or { '' }

vlib/net/http/server_test.v

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ mut:
5252
counter int
5353
oks int
5454
not_founds int
55+
redirects int
5556
}
5657

5758
fn (mut handler MyHttpHandler) handle(req http.Request) http.Response {
@@ -66,6 +67,17 @@ fn (mut handler MyHttpHandler) handle(req http.Request) http.Response {
6667
r.set_status(.ok)
6768
handler.oks++
6869
}
70+
'/redirect_to_big' {
71+
r.header = http.new_header(key: .location, value: '/big')
72+
r.status_msg = 'Moved permanently'
73+
r.status_code = 301
74+
handler.redirects++
75+
}
76+
'/big' {
77+
r.body = 'xyz def '.repeat(10_000)
78+
r.set_status(.ok)
79+
handler.oks++
80+
}
6981
else {
7082
r.set_status(.not_found)
7183
handler.not_founds++
@@ -101,9 +113,56 @@ fn test_server_custom_handler() {
101113
assert y.http_version == '1.1'
102114
//
103115
http.fetch(url: 'http://localhost:${cport}/something/else')!
116+
//
117+
big_url := 'http://localhost:${cport}/redirect_to_big'
118+
mut progress_calls := &ProgressCalls{}
119+
z := http.fetch(
120+
url: big_url
121+
user_ptr: progress_calls
122+
on_redirect: fn (req &http.Request, nredirects int, new_url string) ! {
123+
mut progress_calls := unsafe { &ProgressCalls(req.user_ptr) }
124+
eprintln('>>>>>>>> on_redirect, req.url: ${req.url} | new_url: ${new_url} | nredirects: ${nredirects}')
125+
progress_calls.redirected_to << new_url
126+
}
127+
on_progress: fn (req &http.Request, chunk []u8, read_so_far u64) ! {
128+
mut progress_calls := unsafe { &ProgressCalls(req.user_ptr) }
129+
eprintln('>>>>>>>> on_progress, req.url: ${req.url} | got chunk.len: ${chunk.len:5}, read_so_far: ${read_so_far:8}, chunk: ${chunk#[0..30].bytestr()}')
130+
progress_calls.chunks << chunk
131+
progress_calls.reads << read_so_far
132+
}
133+
on_finish: fn (req &http.Request, final_size u64) ! {
134+
mut progress_calls := unsafe { &ProgressCalls(req.user_ptr) }
135+
eprintln('>>>>>>>> on_finish, req.url: ${req.url}, final_size: ${final_size}')
136+
progress_calls.finished_was_called = true
137+
progress_calls.final_size = final_size
138+
}
139+
)!
140+
assert z.status_code == 200
141+
assert z.body.starts_with('xyz')
142+
assert z.body.len > 10000
143+
assert progress_calls.final_size > 80_000
144+
assert progress_calls.finished_was_called
145+
assert progress_calls.chunks.len > 1
146+
assert progress_calls.reads.len > 1
147+
assert progress_calls.chunks[0].bytestr().starts_with('HTTP/1.1 301 Moved permanently')
148+
assert progress_calls.chunks[1].bytestr().starts_with('HTTP/1.1 200 OK')
149+
assert progress_calls.chunks.last().bytestr().contains('xyz def')
150+
assert progress_calls.redirected_to == ['http://localhost:8198/big']
151+
//
104152
server.stop()
105153
t.wait()
106-
assert handler.counter == 3
107-
assert handler.oks == 2
154+
//
155+
assert handler.counter == 5
156+
assert handler.oks == 3
108157
assert handler.not_founds == 1
158+
assert handler.redirects == 1
159+
}
160+
161+
struct ProgressCalls {
162+
mut:
163+
chunks [][]u8
164+
reads []u64
165+
finished_was_called bool
166+
redirected_to []string
167+
final_size u64
109168
}

0 commit comments

Comments
 (0)