// vtest build: !windows // fasthttp.Server.run is not implemented on windows yet import veb import net.http import os import time import compress.gzip import compress.zstd const port = 13001 const localserver = 'http://127.0.0.1:${port}' const exit_after = time.second * 10 pub struct Context { veb.Context pub mut: counter int bound_middleware_tag string } @[heap] pub struct App { veb.Middleware[Context] mut: started chan bool bound_middleware_hits int bound_middleware_value string } pub fn (mut app App) before_accept_loop() { app.started <- true } pub fn (app &App) index(mut ctx Context) veb.Result { return ctx.text('from index, ${ctx.counter}') } @['/bar/bar'] pub fn (app &App) bar(mut ctx Context) veb.Result { return ctx.text('from bar, ${ctx.counter}') } pub fn (app &App) unreachable(mut ctx Context) veb.Result { return ctx.text('should never be reachable!') } @['/nested/route/method'] pub fn (app &App) nested(mut ctx Context) veb.Result { return ctx.text('from nested, ${ctx.counter}') } pub fn (app &App) after(mut ctx Context) veb.Result { return ctx.text('from after, ${ctx.counter}') } @['/admin/auth'; get] pub fn (app &App) admin_auth_get(mut ctx Context) veb.Result { return ctx.text('protected get route') } @['/admin/auth'; post] pub fn (app &App) admin_auth_post(mut ctx Context) veb.Result { return ctx.text('public post route') } @['/gzip'] pub fn (app &App) gzip_test(mut ctx Context) veb.Result { return ctx.text('gzip response, ${ctx.counter}') } @['/decode_gzip'; post] pub fn (app &App) decode_gzip_test(mut ctx Context) veb.Result { return ctx.text('received: ${ctx.req.data}') } @['/zstd'] pub fn (app &App) zstd_test(mut ctx Context) veb.Result { return ctx.text('zstd response, ${ctx.counter}') } @['/decode_zstd'; post] pub fn (app &App) decode_zstd_test(mut ctx Context) veb.Result { return ctx.text('received: ${ctx.req.data}') } @['/content'] pub fn (app &App) content_test(mut ctx Context) veb.Result { return ctx.text('content response, ${ctx.counter}') } // Test route for double-send regression test @['/double_after'] pub fn (app &App) double_after(mut ctx Context) veb.Result { return ctx.text('handler response') } // bound verifies that route middleware can read app state before the handler runs. @['/bound'] pub fn (app &App) bound(mut ctx Context) veb.Result { return ctx.text('${app.bound_middleware_value}, ${app.bound_middleware_hits}, ${ctx.bound_middleware_tag}, ${ctx.counter}') } pub fn (app &App) app_middleware(mut ctx Context) bool { ctx.counter++ return true } pub fn (app &App) before_request(mut ctx Context) bool { ctx.res.header.add_custom('X-BEFORE-REQUEST-MIDDLEWARE', 'true') or { panic(err) } return true } // route_bound_middleware reads and mutates the bound app to exercise stateful middleware. pub fn (mut app App) route_bound_middleware(mut ctx Context) bool { app.bound_middleware_hits++ ctx.bound_middleware_tag = app.bound_middleware_value ctx.counter += 10 return true } fn middleware_handler(mut ctx Context) bool { ctx.counter++ return true } fn middleware_unreachable(mut ctx Context) bool { ctx.text('unreachable, ${ctx.counter}') return false } fn after_middleware(mut ctx Context) bool { ctx.counter++ ctx.res.header.add_custom('X-AFTER', ctx.counter.str()) or { panic('bad') } return true } fn auth_required_middleware(mut ctx Context) bool { ctx.res.set_status(.unauthorized) ctx.text('auth required') return false } // Global after-middleware that sends a response (for double-send regression test) // Only sends for /double_after route to avoid breaking other tests fn global_after_sends_response(mut ctx Context) bool { if ctx.req.url.starts_with('/double_after') { ctx.text('global after response') } return true } // Route after-middleware that also tries to send (should be skipped if global already sent) fn route_after_sends_response(mut ctx Context) bool { // Add header to prove this middleware ran (for regression test) ctx.res.header.add_custom('X-ROUTE-AFTER-RAN', 'true') or {} ctx.text('route after response - should not appear') return true } fn testsuite_begin() { os.chdir(os.dir(@FILE))! mut app := &App{ bound_middleware_value: 'from app middleware' } // even though `route_use` is called first, global middleware is still executed first app.Middleware.route_use('/unreachable', handler: middleware_unreachable) // global middleware app.Middleware.use(handler: middleware_handler) app.Middleware.use(handler: app.app_middleware) app.Middleware.use(handler: app.before_request) // should match only one slash app.Middleware.route_use('/bar/:foo', handler: middleware_handler) // should match multiple slashes app.Middleware.route_use('/nested/:path...', handler: middleware_handler) app.Middleware.route_use('/bound', handler: app.route_bound_middleware) app.Middleware.route_use('/after', handler: after_middleware, after: true) app.Middleware.route_use('/admin/auth', handler: auth_required_middleware methods: [ .get, ] ) // Gzip middleware tests app.Middleware.use(veb.decode_gzip[Context]()) app.Middleware.route_use('/gzip', veb.encode_gzip[Context]()) // Zstd middleware tests app.Middleware.use(veb.decode_zstd[Context]()) app.Middleware.route_use('/zstd', veb.encode_zstd[Context]()) // Auto content encoding middleware (zstd/gzip based on Accept-Encoding) app.Middleware.route_use('/content', veb.encode_auto[Context]()) // Regression test: global after-middleware sends response, route after-middleware should be skipped app.Middleware.use(handler: global_after_sends_response, after: true) app.Middleware.route_use('/double_after', handler: route_after_sends_response, after: true) spawn veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip) // app startup time _ := <-app.started spawn fn () { time.sleep(exit_after) assert true == false, 'timeout reached!' exit(1) }() } fn test_index() { x := http.get(localserver)! assert x.body == 'from index, 2' } fn test_bound_before_request_named_middleware_does_not_timeout() { x := http.fetch(http.FetchConfig{ url: localserver read_timeout: 1500 * time.millisecond write_timeout: 1500 * time.millisecond })! assert x.body == 'from index, 2' assert x.header.get_custom('X-BEFORE-REQUEST-MIDDLEWARE')! == 'true' } fn test_unreachable_order() { x := http.get('${localserver}/unreachable')! assert x.body == 'unreachable, 2' } fn test_dynamic_route() { x := http.get('${localserver}/bar/bar')! assert x.body == 'from bar, 3' } fn test_nested() { x := http.get('${localserver}/nested/route/method')! assert x.body == 'from nested, 3' } fn test_route_middleware_bound_method_can_access_app_state() { x := http.get('${localserver}/bound')! assert x.body == 'from app middleware, 1, from app middleware, 12' } fn test_route_middleware_method_filter_get() { x := http.get('${localserver}/admin/auth')! assert x.status() == .unauthorized assert x.body == 'auth required' } fn test_route_middleware_method_filter_post() { x := http.fetch(http.FetchConfig{ url: '${localserver}/admin/auth' method: .post })! assert x.status() == .ok assert x.body == 'public post route' } fn test_after_middleware() { x := http.get('${localserver}/after')! assert x.body == 'from after, 2' custom_header := x.header.get_custom('X-AFTER') or { panic('should be set!') } assert custom_header == '3' } // Verifies that encode_gzip compresses responses fn test_encode_gzip_middleware() { x := http.get('${localserver}/gzip')! encoding := x.header.get(.content_encoding) or { '' } assert encoding == 'gzip', 'Expected gzip encoding, got: ${encoding}' // HTTP client auto-decompresses gzip content assert x.body == 'gzip response, 2' } // Verifies that decode_gzip middleware decompresses request bodies fn test_decode_gzip_middleware() { original_text := 'Hello from gzip compressed body!' compressed := gzip.compress(original_text.bytes())! mut req := http.new_request(.post, '${localserver}/decode_gzip', compressed.bytestr()) req.header.add(.content_encoding, 'gzip') x := req.do()! assert x.body == 'received: ${original_text}' } // Verifies that encode_zstd compresses responses fn test_encode_zstd_middleware() { x := http.get('${localserver}/zstd')! encoding := x.header.get(.content_encoding) or { '' } assert encoding == 'zstd', 'Expected zstd encoding, got: ${encoding}' decompressed := zstd.decompress(x.body.bytes())! assert decompressed.bytestr() == 'zstd response, 2' } // Verifies that decode_zstd middleware decompresses request bodies fn test_decode_zstd_middleware() { original_text := 'Hello from zstd compressed body!' compressed := zstd.compress(original_text.bytes())! mut req := http.new_request(.post, '${localserver}/decode_zstd', compressed.bytestr()) req.header.add(.content_encoding, 'zstd') x := req.do()! assert x.body == 'received: ${original_text}' } // Verifies that encode_auto uses zstd when client supports it fn test_encode_auto_zstd() { mut req := http.new_request(.get, '${localserver}/content', '') req.header.add(.accept_encoding, 'gzip, zstd, br') x := req.do()! encoding := x.header.get(.content_encoding) or { '' } assert encoding == 'zstd', 'Expected zstd encoding when zstd is in Accept-Encoding, got: ${encoding}' decompressed := zstd.decompress(x.body.bytes())! assert decompressed.bytestr() == 'content response, 2' } // Verifies that encode_auto falls back to gzip when client doesn't support zstd fn test_encode_auto_gzip_fallback() { mut req := http.new_request(.get, '${localserver}/content', '') req.header.add(.accept_encoding, 'gzip, br') x := req.do()! encoding := x.header.get(.content_encoding) or { '' } assert encoding == 'gzip', 'Expected gzip encoding when zstd is not in Accept-Encoding, got: ${encoding}' // HTTP client auto-decompresses gzip content assert x.body == 'content response, 2' } // Verifies that encode_auto sends uncompressed when no encoding is supported fn test_encode_auto_no_compression() { mut req := http.new_request(.get, '${localserver}/content', '') req.header.add(.accept_encoding, 'br') x := req.do()! encoding := x.header.get(.content_encoding) or { '' } assert encoding == '', 'Expected no encoding when neither gzip nor zstd is in Accept-Encoding, got: ${encoding}' assert x.body == 'content response, 2' } // Regression test: when global after-middleware sends response, route after-middleware should be skipped // This tests the fix for "a response cannot be sent twice over one connection" error fn test_double_after_middleware_no_error() { x := http.get('${localserver}/double_after')! // Global after-middleware response should be received assert x.body == 'global after response' // Route after-middleware should NOT have run (header should be absent) route_after_ran := x.header.get_custom('X-ROUTE-AFTER-RAN') or { '' } assert route_after_ran == '', 'Route after-middleware should be skipped when global already sent response' }