Cro框架中如何多次消费request.body?连接阻塞问题求助
问题描述
我正在编写一个Cro中间件,通过消费request.body做验证,代码如下:
before-matched { request-body -> (:$email, :$captcha-token, :$captcha-solution, *%_) { # 验证邮箱 unless Email::Valid.mx($email).so { response.status = 400; content 'application/json', %(message => 'Invalid Email'); } # 验证验证码 unless $captcha.validate($captcha-token, $captcha-solution) { response.status = 401; content 'application/json', %(message => 'Invalid Captcha'); } } } post -> 'api', 'subscribe' { put "outside"; request-body -> (:$name, :$email, *%_) { put "inside"; dd $name, $email; content 'application/json', %(message => $name); } }
多次消费request.body后连接阻塞,示例里的"inside"从未打印。
可复现代码
use Cro::HTTP::Server; use Cro::HTTP::Router; sub MAIN() { my Cro::Service $http = Cro::HTTP::Server.new( http => <1.1>, host => "127.0.0.1", port => 10000, application => routes() ); $http.start; put "Listening at http://127.0.0.1:10000"; react { whenever signal(SIGINT) { say "Shutting down..."; $http.stop; done; } } } sub routes() { route { before-matched { request-body-text -> $body { put "in before-matched: `{$body}'"; } } post -> { put "in post route before request-body-text"; dd request.body-text; request-body-text -> $body { put "in post route: `{$body}'"; } } } }
请求与现象
用curl -v 'http://127.0.0.1:10000' --data-raw 'some-text'请求后,程序打印以下内容后挂起:
andinus@cadmium /tmp> raku cro-question-mre.raku Listening at http://127.0.0.1:10000 in before-matched: `some-text' in post route before request-body-text Promise.new(scheduler => ThreadPoolScheduler.new(uncaught_handler => Callable), status => PromiseStatus::Planned)
request.body-text返回Promise,但后续无进展。即使只消费一次request.body也有同样问题,请问哪里操作错了?
解决方案
核心问题
HTTP请求体是流式数据,只能被消费一次。第一次在before-matched里用request-body-text读取后,请求体流已经耗尽,后续再尝试读取就会一直等待永远不会完成的Promise,导致连接阻塞。
解决步骤
- 缓存请求体内容:在中间件里读取请求体后,把内容缓存到请求的自定义属性中,后续路由直接使用缓存的内容,不要再尝试重新读取请求体流。
- 避免重复消费流:所有需要使用请求体的逻辑都依赖缓存的内容,不要多次调用
request-body或request-body-text。
修改后的可复现代码
use Cro::HTTP::Server; use Cro::HTTP::Router; sub MAIN() { my Cro::Service $http = Cro::HTTP::Server.new( http => <1.1>, host => "127.0.0.1", port => 10000, application => routes() ); $http.start; put "Listening at http://127.0.0.1:10000"; react { whenever signal(SIGINT) { say "Shutting down..."; $http.stop; done; } } } sub routes() { route { before-matched { request-body-text -> $body { put "in before-matched: `{$body}'"; # 将请求体内容缓存到请求的自定义属性中 request.cache-body = $body; } } post -> { put "in post route before cached body"; # 直接使用缓存的内容,不再读取请求体流 dd request.cache-body; put "in post route: `{request.cache-body}'"; content 'text/plain', request.cache-body; } } }
针对验证场景的优化
在你的验证中间件场景中,读取请求体做验证后,需要把解析后的参数也缓存下来,后续路由直接使用缓存的参数,避免重复解析:
before-matched { request-body -> (:$email, :$captcha-token, :$captcha-solution, *%rest) { # 邮箱验证 unless Email::Valid.mx($email).so { response.status = 400; content 'application/json', %(message => 'Invalid Email'); } # 验证码验证 unless $captcha.validate($captcha-token, $captcha-solution) { response.status = 401; content 'application/json', %(message => 'Invalid Captcha'); } # 缓存解析后的所有参数 request.parsed-body = %(email => $email, captcha-token => $captcha-token, captcha-solution => $captcha-solution, |%rest); } } post -> 'api', 'subscribe' { put "outside"; # 直接使用缓存的解析后参数 my (:$name, :$email) = request.parsed-body; put "inside"; dd $name, $email; content 'application/json', %(message => $name); }
内容的提问来源于stack exchange,提问作者Andinus
相关产品推荐
相关产品推荐

