mirror of
https://github.com/vlang/v.git
synced 2025-09-18 20:07:02 -04:00
x.vweb: support HTTP 1.1 persistent connections (#20658)
This commit is contained in:
parent
10aaeeb54e
commit
32b4a3c008
@ -35,6 +35,8 @@ mut:
|
|||||||
// how the http response should be handled by vweb's backend
|
// how the http response should be handled by vweb's backend
|
||||||
return_type ContextReturnType = .normal
|
return_type ContextReturnType = .normal
|
||||||
return_file string
|
return_file string
|
||||||
|
// If the `Connection: close` header is present the connection should always be closed
|
||||||
|
client_wants_to_close bool
|
||||||
pub:
|
pub:
|
||||||
// TODO: move this to `handle_request`
|
// TODO: move this to `handle_request`
|
||||||
// time.ticks() from start of vweb connection handle.
|
// time.ticks() from start of vweb connection handle.
|
||||||
@ -103,9 +105,9 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, response strin
|
|||||||
}
|
}
|
||||||
// send vweb's closing headers
|
// send vweb's closing headers
|
||||||
ctx.res.header.set(.server, 'VWeb')
|
ctx.res.header.set(.server, 'VWeb')
|
||||||
// sent `Connection: close header` by default, if the user hasn't specified that the
|
if !ctx.takeover && ctx.client_wants_to_close {
|
||||||
// connection should not be closed.
|
// Only sent the `Connection: close` header when the client wants to close
|
||||||
if !ctx.takeover {
|
// the connection. This typically happens when the client only supports HTTP 1.0
|
||||||
ctx.res.header.set(.connection, 'close')
|
ctx.res.header.set(.connection, 'close')
|
||||||
}
|
}
|
||||||
// set the http version
|
// set the http version
|
||||||
|
126
vlib/x/vweb/tests/persistent_connection_test.v
Normal file
126
vlib/x/vweb/tests/persistent_connection_test.v
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import net
|
||||||
|
import net.http
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import x.vweb
|
||||||
|
|
||||||
|
const exit_after = time.second * 10
|
||||||
|
const port = 13009
|
||||||
|
const localserver = 'localhost:${port}'
|
||||||
|
const tcp_r_timeout = 2 * time.second
|
||||||
|
const tcp_w_timeout = 2 * time.second
|
||||||
|
const max_retries = 4
|
||||||
|
|
||||||
|
const default_request = 'GET / HTTP/1.1
|
||||||
|
User-Agent: VTESTS
|
||||||
|
Accept: */*
|
||||||
|
\r\n'
|
||||||
|
|
||||||
|
const response_body = 'intact!'
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
vweb.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
mut:
|
||||||
|
started chan bool
|
||||||
|
counter int
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (mut app App) before_accept_loop() {
|
||||||
|
app.started <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (mut app App) index(mut ctx Context) vweb.Result {
|
||||||
|
app.counter++
|
||||||
|
return ctx.text('${response_body}:${app.counter}')
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (mut app App) reset(mut ctx Context) vweb.Result {
|
||||||
|
app.counter = 0
|
||||||
|
return ctx.ok('')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn testsuite_begin() {
|
||||||
|
os.chdir(os.dir(@FILE))!
|
||||||
|
mut app := &App{}
|
||||||
|
|
||||||
|
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 5)
|
||||||
|
_ := <-app.started
|
||||||
|
|
||||||
|
spawn fn () {
|
||||||
|
time.sleep(exit_after)
|
||||||
|
assert true == false, 'timeout reached!'
|
||||||
|
exit(1)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_conn_remains_intact() {
|
||||||
|
http.get('http://${localserver}/reset')!
|
||||||
|
|
||||||
|
mut conn := simple_tcp_client()!
|
||||||
|
conn.write_string(default_request)!
|
||||||
|
|
||||||
|
mut read := io.read_all(reader: conn)!
|
||||||
|
mut response := read.bytestr()
|
||||||
|
assert response.contains('Connection: close') == false, '`Connection` header should NOT be present!'
|
||||||
|
assert response.ends_with('${response_body}:1') == true, 'read response: ${response}'
|
||||||
|
|
||||||
|
// send request again over the same connection
|
||||||
|
conn.write_string(default_request)!
|
||||||
|
|
||||||
|
read = io.read_all(reader: conn)!
|
||||||
|
response = read.bytestr()
|
||||||
|
assert response.contains('Connection: close') == false, '`Connection` header should NOT be present!'
|
||||||
|
assert response.ends_with('${response_body}:2') == true, 'read response: ${response}'
|
||||||
|
|
||||||
|
conn.close() or {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_support_http_1() {
|
||||||
|
http.get('http://${localserver}/reset')!
|
||||||
|
// HTTP 1.0 always closes the connection after each request, so the client must
|
||||||
|
// send the Connection: close header. If that header is present the connection
|
||||||
|
// needs to be closed and a `Connection: close` header needs to be send back
|
||||||
|
mut x := http.fetch(http.FetchConfig{
|
||||||
|
url: 'http://${localserver}/'
|
||||||
|
header: http.new_header_from_map({
|
||||||
|
.connection: 'close'
|
||||||
|
})
|
||||||
|
})!
|
||||||
|
assert x.status() == .ok
|
||||||
|
if conn_header := x.header.get(.connection) {
|
||||||
|
assert conn_header == 'close'
|
||||||
|
} else {
|
||||||
|
assert false, '`Connection: close` header should be present!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility code:
|
||||||
|
|
||||||
|
fn simple_tcp_client() !&net.TcpConn {
|
||||||
|
mut client := &net.TcpConn(unsafe { nil })
|
||||||
|
mut tries := 0
|
||||||
|
for tries < max_retries {
|
||||||
|
tries++
|
||||||
|
eprintln('> client retries: ${tries}')
|
||||||
|
client = net.dial_tcp(localserver) or {
|
||||||
|
eprintln('dial error: ${err.msg()}')
|
||||||
|
if tries > max_retries {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.sleep(100 * time.millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if client == unsafe { nil } {
|
||||||
|
eprintln('could not create a tcp client connection to http://${localserver} after ${max_retries} retries')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
client.set_read_timeout(tcp_r_timeout)
|
||||||
|
client.set_write_timeout(tcp_w_timeout)
|
||||||
|
return client
|
||||||
|
}
|
@ -111,7 +111,6 @@ fn assert_common_http_headers(x http.Response) ! {
|
|||||||
assert x.status() == .ok
|
assert x.status() == .ok
|
||||||
assert x.header.get(.server)! == 'VWeb'
|
assert x.header.get(.server)! == 'VWeb'
|
||||||
assert x.header.get(.content_length)!.int() > 0
|
assert x.header.get(.content_length)!.int() > 0
|
||||||
assert x.header.get(.connection)! == 'close'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_http_client_index() {
|
fn test_http_client_index() {
|
||||||
@ -119,6 +118,7 @@ fn test_http_client_index() {
|
|||||||
assert_common_http_headers(x)!
|
assert_common_http_headers(x)!
|
||||||
assert x.header.get(.content_type)! == 'text/plain'
|
assert x.header.get(.content_type)! == 'text/plain'
|
||||||
assert x.body == 'Welcome to VWeb'
|
assert x.body == 'Welcome to VWeb'
|
||||||
|
assert x.header.get(.connection)! == 'close'
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_http_client_404() {
|
fn test_http_client_404() {
|
||||||
@ -327,6 +327,7 @@ fn simple_tcp_client(config SimpleTcpClientConfig) !string {
|
|||||||
Host: ${config.host}
|
Host: ${config.host}
|
||||||
User-Agent: ${config.agent}
|
User-Agent: ${config.agent}
|
||||||
Accept: */*
|
Accept: */*
|
||||||
|
Connection: close
|
||||||
${config.headers}
|
${config.headers}
|
||||||
${config.content}'
|
${config.content}'
|
||||||
$if debug_net_socket_client ? {
|
$if debug_net_socket_client ? {
|
||||||
|
@ -29,8 +29,7 @@ pub fn no_result() Result {
|
|||||||
pub const methods_with_form = [http.Method.post, .put, .patch]
|
pub const methods_with_form = [http.Method.post, .put, .patch]
|
||||||
|
|
||||||
pub const headers_close = http.new_custom_header_from_map({
|
pub const headers_close = http.new_custom_header_from_map({
|
||||||
'Server': 'VWeb'
|
'Server': 'VWeb'
|
||||||
http.CommonHeader.connection.str(): 'close'
|
|
||||||
}) or { panic('should never fail') }
|
}) or { panic('should never fail') }
|
||||||
|
|
||||||
pub const http_302 = http.new_response(
|
pub const http_302 = http.new_response(
|
||||||
@ -221,10 +220,11 @@ pub struct RunParams {
|
|||||||
|
|
||||||
struct FileResponse {
|
struct FileResponse {
|
||||||
pub mut:
|
pub mut:
|
||||||
open bool
|
open bool
|
||||||
file os.File
|
file os.File
|
||||||
total i64
|
total i64
|
||||||
pos i64
|
pos i64
|
||||||
|
should_close_conn bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// close the open file and reset the struct to its default values
|
// close the open file and reset the struct to its default values
|
||||||
@ -233,13 +233,15 @@ pub fn (mut fr FileResponse) done() {
|
|||||||
fr.file.close()
|
fr.file.close()
|
||||||
fr.total = 0
|
fr.total = 0
|
||||||
fr.pos = 0
|
fr.pos = 0
|
||||||
|
fr.should_close_conn = false
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StringResponse {
|
struct StringResponse {
|
||||||
pub mut:
|
pub mut:
|
||||||
open bool
|
open bool
|
||||||
str string
|
str string
|
||||||
pos i64
|
pos i64
|
||||||
|
should_close_conn bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// free the current string and reset the struct to its default values
|
// free the current string and reset the struct to its default values
|
||||||
@ -247,6 +249,7 @@ pub mut:
|
|||||||
pub fn (mut sr StringResponse) done() {
|
pub fn (mut sr StringResponse) done() {
|
||||||
sr.open = false
|
sr.open = false
|
||||||
sr.pos = 0
|
sr.pos = 0
|
||||||
|
sr.should_close_conn = false
|
||||||
unsafe { sr.str.free() }
|
unsafe { sr.str.free() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,7 +421,7 @@ fn handle_write_file(mut pv picoev.Picoev, mut params RequestParams, fd int) {
|
|||||||
if params.file_responses[fd].pos == params.file_responses[fd].total {
|
if params.file_responses[fd].pos == params.file_responses[fd].total {
|
||||||
// file is done writing
|
// file is done writing
|
||||||
params.file_responses[fd].done()
|
params.file_responses[fd].done()
|
||||||
pv.close_conn(fd)
|
handle_complete_request(params.file_responses[fd].should_close_conn, mut pv, fd)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -450,6 +453,8 @@ fn handle_write_string(mut pv picoev.Picoev, mut params RequestParams, fd int) {
|
|||||||
// done writing
|
// done writing
|
||||||
params.string_responses[fd].done()
|
params.string_responses[fd].done()
|
||||||
pv.close_conn(fd)
|
pv.close_conn(fd)
|
||||||
|
handle_complete_request(params.string_responses[fd].should_close_conn, mut pv,
|
||||||
|
fd)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -490,6 +495,8 @@ fn handle_read[A, X](mut pv picoev.Picoev, mut params RequestParams, fd int) {
|
|||||||
if err !is io.Eof {
|
if err !is io.Eof {
|
||||||
eprintln('[vweb] error parsing request: ${err}')
|
eprintln('[vweb] error parsing request: ${err}')
|
||||||
}
|
}
|
||||||
|
// the buffered reader was empty meaning that the client probably
|
||||||
|
// closed the connection.
|
||||||
pv.close_conn(fd)
|
pv.close_conn(fd)
|
||||||
params.incomplete_requests[fd] = http.Request{}
|
params.incomplete_requests[fd] = http.Request{}
|
||||||
return
|
return
|
||||||
@ -588,7 +595,8 @@ fn handle_read[A, X](mut pv picoev.Picoev, mut params RequestParams, fd int) {
|
|||||||
// See Context.send_file for why we use max_read instead of max_write.
|
// See Context.send_file for why we use max_read instead of max_write.
|
||||||
if completed_context.res.body.len < vweb.max_read {
|
if completed_context.res.body.len < vweb.max_read {
|
||||||
fast_send_resp(mut conn, completed_context.res) or {}
|
fast_send_resp(mut conn, completed_context.res) or {}
|
||||||
pv.close_conn(fd)
|
handle_complete_request(completed_context.client_wants_to_close, mut
|
||||||
|
pv, fd)
|
||||||
} else {
|
} else {
|
||||||
params.string_responses[fd].open = true
|
params.string_responses[fd].open = true
|
||||||
params.string_responses[fd].str = completed_context.res.body
|
params.string_responses[fd].str = completed_context.res.body
|
||||||
@ -599,7 +607,8 @@ fn handle_read[A, X](mut pv picoev.Picoev, mut params RequestParams, fd int) {
|
|||||||
// should not happen
|
// should not happen
|
||||||
params.string_responses[fd].done()
|
params.string_responses[fd].done()
|
||||||
fast_send_resp(mut conn, vweb.http_500) or {}
|
fast_send_resp(mut conn, vweb.http_500) or {}
|
||||||
pv.close_conn(fd)
|
handle_complete_request(completed_context.client_wants_to_close, mut
|
||||||
|
pv, fd)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// no errors we can send the HTTP headers
|
// no errors we can send the HTTP headers
|
||||||
@ -636,6 +645,15 @@ fn handle_read[A, X](mut pv picoev.Picoev, mut params RequestParams, fd int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// invalid request headers/data
|
||||||
|
pv.close_conn(fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// close the connection when `should_close` is true.
|
||||||
|
@[inline]
|
||||||
|
fn handle_complete_request(should_close bool, mut pv picoev.Picoev, fd int) {
|
||||||
|
if should_close {
|
||||||
pv.close_conn(fd)
|
pv.close_conn(fd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -681,6 +699,14 @@ fn handle_request[A, X](mut conn net.TcpConn, req http.Request, params &RequestP
|
|||||||
files: files
|
files: files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if connection_header := req.header.get(.connection) {
|
||||||
|
// A client that does not support persistent connections MUST send the
|
||||||
|
// "close" connection option in every request message.
|
||||||
|
if connection_header.to_lower() == 'close' {
|
||||||
|
ctx.client_wants_to_close = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$if A is StaticApp {
|
$if A is StaticApp {
|
||||||
ctx.custom_mime_types = global_app.static_mime_types.clone()
|
ctx.custom_mime_types = global_app.static_mime_types.clone()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user