Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions dispatch.mbt
Original file line number Diff line number Diff line change
@@ -1,17 +1,56 @@
///|
/// Split a raw HTTP request-target into its path and query string.
///
/// Routes match against the path; the query is retained separately on the
/// `HttpRequest`. A `#fragment` is never used for routing, so it is stripped
/// from the query rather than exposed.
fn split_request_target(target : String) -> (String, String) {
let (path, rest) = match target.find("?") {
Some(q) => (target[:q].to_owned(), target[q + 1:].to_owned())
None => (target, "")
}
let query = match rest.find("#") {
Some(f) => rest[:f].to_owned()
None => rest
}
(path, query)
}

///|
/// Whether a request carries a body: POST/PUT/PATCH always may, and any
/// method with an explicit (non-empty) `content-length` or a
/// `transfer-encoding` (i.e. chunked) frame is treated as having a body.
/// The same rule is used by every backend so bodies are read consistently.
fn request_has_body(
http_method : String,
headers : Map[@http.CaseInsensitiveString, StringView],
) -> Bool {
match http_method {
"POST" | "PUT" | "PATCH" => true
_ =>
headers.get("transfer-encoding") is Some(_) ||
headers
.get("content-length")
.map(value => value.to_owned().trim() != "0")
.unwrap_or(false)
}
}

///|
pub async fn dispatch_http(
mocket : Mocket,
http_method : String,
url : String,
headers : Map[StringView, StringView],
headers : Map[@http.CaseInsensitiveString, StringView],
raw_body : Bytes,
) -> HttpResponse {
let (params, handler) = match mocket.find_route(http_method, url) {
let (path, query) = split_request_target(url)
let (params, handler) = match mocket.find_route(http_method, path) {
Some((h, p)) => (p, h)
_ => ({}, handle_not_found())
}
let event = {
req: { http_method, url, raw_body, headers },
req: { http_method, url: path, query, raw_body, headers },
res: HttpResponse::new(OK),
params,
}
Expand Down
60 changes: 23 additions & 37 deletions mocket.js.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,8 @@ fn listen_port(address : String) -> Int {
pub fn listen_ffi(mocket : Mocket, address : String) -> Unit {
let port = listen_port(address)
let server = create_server(fn(req, res, _) {
// 优化:简化 headers 转换逻辑
let string_headers : Map[StringView, StringView] = Map([])
// 构造大小写不敏感的头部映射表(HTTP 字段名不区分大小写)
let string_headers : Map[@http.CaseInsensitiveString, StringView] = Map([])
let json_val = req.headers().to_value().to_json() catch {
_ => {
res.write_head(400, @js.Object::new().to_value())
Expand All @@ -322,34 +322,22 @@ pub fn listen_ffi(mocket : Mocket, address : String) -> Unit {
return
}

// 批量转换 headers,减少单个处理的开销
headers.each(fn(key, value) {
if value is String(v) {
string_headers.set(key, v.to_string_view())
}
})
let (params, handler) = match
mocket.find_route(req.req_method(), req.url()) {
Some((h, p)) => (p, h)
_ => ({}, handle_not_found())
}
let event = {
req: {
http_method: req.req_method(),
url: req.url(),
headers: string_headers,
raw_body: b"",
},
res: HttpResponse::new(OK),
params,
}

let http_method = req.req_method()
let url = req.url()
let should_read_body = request_has_body(http_method, string_headers)
async_run(() => {
// 如果是 post,先等待 data 事件
if event.req.http_method == "POST" {
let mut raw = b""
if should_read_body {
let buffer = Buffer()
let mut total_size = 0
let mut exceeded = false
suspend(fn(res, _) {
suspend(fn(done, _) {
req.on("data", data => {
if !exceeded {
let chunk = node_body_chunk_to_bytes(data)
Expand All @@ -361,7 +349,7 @@ pub fn listen_ffi(mocket : Mocket, address : String) -> Unit {
}
}
})
req.on("end", _ => res(()))
req.on("end", _ => done(()))
}) catch {
_ => ()
}
Expand All @@ -370,35 +358,33 @@ pub fn listen_ffi(mocket : Mocket, address : String) -> Unit {
res.end(@js.Value::cast_from("Request body too large"))
return
}
event.req.raw_body = buffer.to_bytes()
raw = buffer.to_bytes()
}

// 执行中间件链和处理器
let responder = mocket.execute_middlewares(event, handler) catch {
err => mocket.handle_request_error(event, err)
// 交给统一的 dispatch_http:路由、查询拆分、中间件与错误处理全部一致。
let response = dispatch_http(
mocket, http_method, url, string_headers, raw,
) catch {
_ =>
HttpResponse::new(InternalServerError).body("Internal Server Error")
}
// let boundary = "----------------moonbit-" + port.to_string()
responder.options(event.res)
res.write_head(
event.res.status_code.to_int(),
response.status_code.to_int(),
{
let headers_obj = @js.Value::from_json(event.res.headers.to_json()) catch {
let mut headers_obj = @js.Value::from_json(response.headers.to_json()) catch {
_ => @js.Object::new().to_value()
}
if !event.res.cookies.is_empty() {
let cookies = event.res.cookies
if !response.cookies.is_empty() {
let cookies = response.cookies
.values()
.map(fn(c) { Show::to_string(c) })
.map(fn(cookie) { Show::to_string(cookie) })
.to_array()
set_js_property(headers_obj, "Set-Cookie", array_to_js(cookies))
}
headers_obj
},
)
let buf = Buffer()
responder.output(buf)
event.res.raw_body = buf.to_bytes()
res.end(@js.Value::cast_from(event.res.raw_body))
res.end(@js.Value::cast_from(response.raw_body))
})
})
start_server(server, address, websocket_accept_key)
Expand Down
44 changes: 15 additions & 29 deletions mocket.native.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,18 @@ fn request_method_to_string(meth : @http.RequestMethod) -> String {
///|
fn string_headers_to_views(
headers : Map[@http.CaseInsensitiveString, String],
) -> Map[StringView, StringView] {
let out : Map[StringView, StringView] = Map([])
headers.each((key, value) => out.set(Show::to_string(key), value))
) -> Map[@http.CaseInsensitiveString, StringView] {
let out : Map[@http.CaseInsensitiveString, StringView] = Map([])
headers.each((key, value) => out.set(key, value))
out
}

///|
fn view_headers_to_strings(
headers : Map[StringView, StringView],
headers : Map[@http.CaseInsensitiveString, StringView],
) -> Map[@http.CaseInsensitiveString, String] {
let out : Map[@http.CaseInsensitiveString, String] = Map([])
headers.each((key, value) => out.set(key.to_owned(), value.to_owned()))
headers.each((key, value) => out.set(key, value.to_owned()))
out
}

Expand Down Expand Up @@ -121,27 +121,6 @@ fn is_websocket_upgrade(request : @http.Request) -> Bool {
header_equals(request.headers, "upgrade", "websocket")
}

///|
fn request_has_body(request : @http.Request) -> Bool {
match request.meth {
Post | Put | Patch => true
_ =>
request.headers.get("transfer-encoding") is Some(_) ||
request.headers
.get("content-length")
.map(value => value.trim() != "0")
.unwrap_or(false)
}
}

///|
fn request_route_path(path : String) -> String {
match path.find("?") {
Some(query_start) => path[:query_start].to_owned()
None => path
}
}

///|
fn find_ws_route(
mocket : Mocket,
Expand Down Expand Up @@ -374,14 +353,20 @@ async fn send_native_response(
conn.end_response()
}

///|

///|
async fn handle_http_request(
mocket : Mocket,
request : @http.Request,
body_reader : &@io.Reader,
conn : @http.ServerConnection,
) -> Unit {
let raw_body = if request_has_body(request) {
let headers = string_headers_to_views(request.headers)
let raw_body = if request_has_body(
request_method_to_string(request.meth),
headers,
) {
let content_length = request.headers
.get("content-length")
.map(s => @string.parse_int(s.trim()) catch { _ => 0 })
Expand All @@ -398,11 +383,12 @@ async fn handle_http_request(
} else {
b""
}
// `dispatch_http` normalizes `request.path` into a path + query internally.
let response = dispatch_http(
mocket,
request_method_to_string(request.meth),
request_route_path(request.path),
string_headers_to_views(request.headers),
request.path,
headers,
raw_body,
) catch {
err => {
Expand Down
8 changes: 5 additions & 3 deletions native/mongoose/mongoose.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,10 @@ fn from_cbytes(bytes : Bytes) -> String {
}

///|
fn parse_headers(headers_text : String) -> Map[StringView, StringView] {
let headers : Map[StringView, StringView] = Map([])
fn parse_headers(
headers_text : String,
) -> Map[@http.CaseInsensitiveString, StringView] {
let headers : Map[@http.CaseInsensitiveString, StringView] = Map([])
headers_text
.split("\n")
.each(line => {
Expand All @@ -129,7 +131,7 @@ fn parse_headers(headers_text : String) -> Map[StringView, StringView] {
let key = line[:idx].trim()
let value = line[idx + 1:].trim()
if key != "" {
headers.set(key, value)
headers.set(key.to_owned(), value)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions native/mongoose/moon.pkg
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
"oboard/mocket",
"moonbitlang/async/http",
"moonbitlang/core/encoding/utf8",
"moonbitlang/core/string",
}
Expand Down
10 changes: 6 additions & 4 deletions pkg.generated.mbti
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package "oboard/mocket"

import {
"moonbitlang/async/http",
"moonbitlang/core/buffer",
"moonbitlang/core/json",
}
Expand All @@ -11,7 +12,7 @@ pub fn __ws_emit(Bytes, Bytes, Bytes) -> Unit

pub fn cookie_to_string(Array[CookieItem]) -> String

pub async fn dispatch_http(Mocket, String, String, Map[StringView, StringView], Bytes) -> HttpResponse
pub async fn dispatch_http(Mocket, String, String, Map[@http.CaseInsensitiveString, StringView], Bytes) -> HttpResponse

pub fn dispatch_ws_event((WebSocketEvent) -> Unit, WebSocketPeer, String, Bytes) -> Unit

Expand Down Expand Up @@ -91,7 +92,8 @@ pub impl Responder for Html
pub(all) struct HttpRequest {
http_method : String
url : String
headers : Map[StringView, StringView]
query : String
headers : Map[@http.CaseInsensitiveString, StringView]
mut raw_body : Bytes
}
pub fn[T : BodyReader] HttpRequest::body(Self) -> T raise
Expand All @@ -101,14 +103,14 @@ pub impl Responder for HttpRequest

pub(all) struct HttpResponse {
mut status_code : StatusCode
headers : Map[StringView, StringView]
headers : Map[@http.CaseInsensitiveString, StringView]
cookies : Map[String, CookieItem]
mut raw_body : Bytes
}
pub fn HttpResponse::body(Self, &Responder) -> Self
pub fn HttpResponse::delete_cookie(Self, String) -> Unit
pub fn HttpResponse::json(Self, &ToJson) -> Self
pub fn HttpResponse::new(StatusCode, headers? : Map[StringView, StringView], cookies? : Map[String, CookieItem], raw_body? : Bytes) -> Self
pub fn HttpResponse::new(StatusCode, headers? : Map[@http.CaseInsensitiveString, StringView], cookies? : Map[String, CookieItem], raw_body? : Bytes) -> Self
pub fn[T : BodyReader] HttpResponse::read_body(Self) -> T raise
pub fn HttpResponse::set_cookie(Self, String, String, max_age? : Int, path? : String, domain? : String, secure? : Bool, http_only? : Bool, same_site? : SameSiteOption) -> Unit
pub fn HttpResponse::to_responder(Self) -> &Responder
Expand Down
9 changes: 8 additions & 1 deletion request.mbt
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
///|
pub(all) struct HttpRequest {
http_method : String
/// The request-target path, without query string or fragment.
/// Routing matches against this value.
url : String
headers : Map[StringView, StringView]
/// The raw query string without the leading `?`; empty when absent.
/// Fragments (`#...`) are stripped.
query : String
/// Case-insensitive request headers (HTTP field names are case-insensitive).
headers : Map[@http.CaseInsensitiveString, StringView]
mut raw_body : Bytes
}

Expand Down Expand Up @@ -67,6 +73,7 @@ test "read_body" {
let req = HttpRequest::{
http_method: "POST",
url: "/",
query: "",
headers: Map([]),
raw_body: b"{\"Hello\":\"World!\"}",
}
Expand Down
Loading