hyperlane-core 22.0.7

A lightweight, high-performance, and cross-platform Rust HTTP server library built on Tokio. It simplifies modern web service development by providing built-in support for middleware, WebSocket, Server-Sent Events (SSE), and raw TCP communication. With a unified and ergonomic API across Windows, Linux, and MacOS, it enables developers to build robust, scalable, and event-driven network applications with minimal overhead and maximum flexibility.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
use super::*;

#[tokio::test]
async fn http1_plaintext_serves_default_protocol_over_tcp() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, None)
            .await
            .expect("http1 plaintext server must start");
    let request: Vec<u8> = http1_close_request("GET", VERSION_ROUTE_PATH);
    let raw: Vec<u8> = raw_http1_exchange(port, &request)
        .await
        .expect("plaintext http1 exchange must complete");
    let head: String = response_head(&raw);
    assert!(
        head.starts_with("HTTP/1.1 200"),
        "expected an HTTP/1.1 200 head, got: {head}"
    );
    let (_, body): (String, Vec<u8>) = response_head_and_body(&raw);
    assert_eq!(
        String::from_utf8_lossy(&body),
        format!("{:?}", HttpVersion::Http1_1)
    );
    control.shutdown().await;
}

#[tokio::test]
async fn http1_plaintext_reads_path_query_and_header_through_middleware() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, None)
            .await
            .expect("http1 plaintext server must start");
    let target: String = format!("{PROBE_PATH}?{}", PROBE_QUERY);
    let request: Vec<u8> = http1_request_with_headers("GET", &target, PROBE_REPEATED_HEADERS, &[]);
    let raw: Vec<u8> = raw_http1_exchange(port, &request)
        .await
        .expect("plaintext http1 exchange must complete");
    let head: String = response_head(&raw);
    assert_eq!(
        response_header_value(&head, PROBE_METHOD_HEADER),
        PROBE_METHOD,
        "the handler must observe the exact request method, got: {head}"
    );
    assert_eq!(
        response_header_value(&head, PROBE_PATH_HEADER),
        PROBE_PATH,
        "the handler must observe the exact request path, got: {head}"
    );
    assert_eq!(
        response_header_value(&head, PROBE_QUERY_HEADER),
        PROBE_QUERY_EXPECTED,
        "every query key must be read back with its value, got: {head}"
    );
    assert_eq!(
        response_header_value(&head, PROBE_HEADER_ECHO_HEADER),
        PROBE_HEADER_EXPECTED,
        "a repeated request header must keep every value in order, got: {head}"
    );
    assert_eq!(
        response_header_value(&head, PROBE_VERSION_HEADER),
        format!("{:?}", HttpVersion::Http1_1),
        "the handler must observe HTTP/1.1, got: {head}"
    );
    control.shutdown().await;
}

#[tokio::test]
async fn http1_tls_negotiates_http11_alpn_against_validated_chain() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, Some(trusted_tls_config()))
            .await
            .expect("http1 tls server must start");
    let stream: ClientTlsStream = connect_tls(port, ALPN_HTTP1)
        .await
        .expect("validating client must complete the handshake");
    let alpn: Option<Vec<u8>> = negotiated_alpn(&stream);
    assert_eq!(
        alpn,
        Some(ALPN_HTTP1.to_vec()),
        "the server must select the http/1.1 ALPN identifier"
    );
    let request: Vec<u8> = http1_close_request("GET", VERSION_ROUTE_PATH);
    let raw: Vec<u8> = tls_exchange(stream, request)
        .await
        .expect("tls http1 exchange must complete");
    let head: String = response_head(&raw);
    assert!(
        head.starts_with("HTTP/1.1 200"),
        "expected HTTP/1.1 over TLS, got: {head}"
    );
    control.shutdown().await;
}

#[tokio::test]
async fn http1_tls_rejects_a_client_whose_alpn_never_matches() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, Some(trusted_tls_config()))
            .await
            .expect("http1 tls server must start");
    let outcome: io::Result<ClientTlsStream> = connect_tls(port, ALPN_HTTP2).await;
    assert!(
        outcome.is_err(),
        "an http1 listener must refuse an h2-only client instead of silently downgrading"
    );
    let valid: io::Result<ClientTlsStream> = connect_tls(port, ALPN_HTTP1).await;
    assert!(
        valid.is_ok(),
        "a rejected client must not disturb a later valid connection"
    );
    control.shutdown().await;
}

#[tokio::test]
async fn http1_plaintext_binary_post_round_trips_hostile_bytes() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, None)
            .await
            .expect("http1 plaintext server must start");
    let payload: Vec<u8> = hostile_binary_payload();
    let request: Vec<u8> = http1_request("POST", BINARY_ROUTE_PATH, &payload);
    let raw: Vec<u8> = raw_http1_exchange(port, &request)
        .await
        .expect("binary post must complete");
    let (_, body): (String, Vec<u8>) = response_head_and_body(&raw);
    assert_eq!(
        body, payload,
        "every hostile byte must survive the http1 body round trip"
    );
    control.shutdown().await;
}

#[tokio::test]
async fn http1_plaintext_refuses_a_truncated_content_length_body() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, None)
            .await
            .expect("http1 plaintext server must start");
    let head: Vec<u8> = format!(
        "POST {BINARY_ROUTE_PATH} HTTP/1.1{HTTP_BR}Host: {TLS_SERVER_NAME}{HTTP_BR}Content-Length: 100{HTTP_BR}{HTTP_BR}"
    )
    .into_bytes();
    let mut request: Vec<u8> = head;
    request.extend_from_slice(b"only-ten!!");
    if let Ok(raw) = raw_http1_exchange(port, &request).await {
        let text: String = response_head(&raw);
        assert!(
            !text.contains(" 200"),
            "a truncated body must not be answered as a complete request, got: {text}"
        );
    }
    control.shutdown().await;
}

#[tokio::test]
async fn http1_plaintext_streams_split_writes_before_the_handler_finishes() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, None)
            .await
            .expect("http1 plaintext server must start");
    let request: Vec<u8> = http1_close_request("GET", SPLIT_ROUTE_PATH);
    let raw: Vec<u8> = raw_http1_exchange(port, &request)
        .await
        .expect("split write exchange must complete");
    let head: String = response_head(&raw);
    assert!(
        !head.to_lowercase().contains(CONTENT_LENGTH),
        "an event-stream response must not be framed with a content-length, got: {head}"
    );
    let (_, body): (String, Vec<u8>) = response_head_and_body(&raw);
    let text: String = String::from_utf8_lossy(&body).to_string();
    assert_eq!(
        text,
        format!("{SPLIT_HEAD_BODY}{SPLIT_TAIL_BODY}"),
        "every separately written fragment must reach the client in order, got: {text:?}"
    );
    control.shutdown().await;
}

#[tokio::test]
async fn http1_tls_streams_split_writes_through_the_tls_listener() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, Some(trusted_tls_config()))
            .await
            .expect("http1 tls server must start");
    let stream: ClientTlsStream = connect_tls(port, ALPN_HTTP1)
        .await
        .expect("validating client must complete the handshake");
    let request: Vec<u8> = http1_close_request("GET", SPLIT_ROUTE_PATH);
    let raw: Vec<u8> = tls_exchange(stream, request)
        .await
        .expect("split write exchange must complete");
    let head: String = response_head(&raw);
    assert!(
        !head.to_lowercase().contains(CONTENT_LENGTH),
        "an event-stream response must not be framed with a content-length, got: {head}"
    );
    let (_, body): (String, Vec<u8>) = response_head_and_body(&raw);
    let text: String = String::from_utf8_lossy(&body).to_string();
    assert_eq!(
        text,
        format!("{SPLIT_HEAD_BODY}{SPLIT_TAIL_BODY}"),
        "every separately written fragment must survive TLS in order, got: {text:?}"
    );
    control.shutdown().await;
}

#[tokio::test]
async fn http1_plaintext_reports_the_real_local_and_peer_addresses_of_the_socket() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, None)
            .await
            .expect("http1 plaintext server must start");
    let mut stream: TcpStream = connect_tcp(port).await.expect("socket must connect");
    let client_local: SocketAddr = stream
        .local_addr()
        .expect("a connected socket must report its local address");
    let request: Vec<u8> = http1_close_request("GET", SOCKET_METADATA_ROUTE_PATH);
    let raw: Vec<u8> = exchange_on(&mut stream, &request).await;
    let (_, body): (String, Vec<u8>) = response_head_and_body(&raw);
    let text: String = String::from_utf8_lossy(&body).to_string();
    let (reported_local, reported_peer): (String, String) =
        match text.split_once(SOCKET_METADATA_SEPARATOR) {
            Some(pair) => (pair.0.to_string(), pair.1.to_string()),
            None => panic!("the route must report both addresses, got: {text:?}"),
        };
    let expected_local: SocketAddr = format!("{LOOPBACK_HOST}:{port}")
        .parse()
        .expect("the bind address must parse");
    assert_eq!(
        reported_local,
        expected_local.to_string(),
        "the handler must see the bound local address, not a fabricated one"
    );
    assert_eq!(
        reported_peer,
        client_local.to_string(),
        "the handler must see this connection's own peer address"
    );
    control.shutdown().await;
}

#[tokio::test]
async fn http1_sse_chunk_reaches_the_client_before_the_handler_finishes() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, None)
            .await
            .expect("http1 plaintext server must start");
    let _lease: GateLease = gate_lease(GateSlot::SseHttp1).await;
    let mut stream: TcpStream = connect_tcp(port).await.expect("socket must connect");
    let request: Vec<u8> = http1_close_request("GET", SSE_ROUTE_PATH);
    write_bounded(&mut stream, &request)
        .await
        .expect("sse request must be written");
    let mut received: Vec<u8> = Vec::new();
    let first_chunk_deadline: Instant = Instant::now() + Duration::from_secs(REQUEST_TIMEOUT_SECS);
    while !String::from_utf8_lossy(&received).contains(SSE_FIRST_CHUNK) {
        assert!(
            Instant::now() < first_chunk_deadline,
            "the first sse chunk must arrive while the handler is still blocked, got: {:?}",
            String::from_utf8_lossy(&received)
        );
        read_more(&mut stream, &mut received).await;
    }
    assert!(
        !String::from_utf8_lossy(&received).contains(SSE_SECOND_CHUNK),
        "the second chunk must not appear before the handler is released"
    );
    route_gate(GateSlot::SseHttp1).release_sse();
    let completion_deadline: Instant = Instant::now() + Duration::from_secs(REQUEST_TIMEOUT_SECS);
    while !String::from_utf8_lossy(&received).contains(SSE_SECOND_CHUNK) {
        assert!(
            Instant::now() < completion_deadline,
            "the second sse chunk must arrive once the handler is released"
        );
        read_more(&mut stream, &mut received).await;
    }
    control.shutdown().await;
}

#[tokio::test]
async fn http1_tls_sse_chunk_reaches_the_client_before_the_handler_finishes() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, Some(trusted_tls_config()))
            .await
            .expect("http1 tls server must start");
    let _lease: GateLease = gate_lease(GateSlot::SseHttp1).await;
    let mut stream: ClientTlsStream = connect_tls(port, ALPN_HTTP1)
        .await
        .expect("validating client must complete the handshake");
    let request: Vec<u8> = http1_close_request("GET", SSE_ROUTE_PATH);
    write_bounded(&mut stream, &request)
        .await
        .expect("sse request must be written");
    let first: String = http1_head_until(&mut stream, SSE_FIRST_CHUNK).await;
    assert!(
        first.contains(SSE_FIRST_CHUNK),
        "the first chunk must arrive over tls while the handler is blocked, got: {first:?}"
    );
    assert!(
        !first.contains(SSE_SECOND_CHUNK),
        "the second chunk must wait for the handler to be released, got: {first:?}"
    );
    route_gate(GateSlot::SseHttp1).release_sse();
    let second: String = http1_head_until(&mut stream, SSE_SECOND_CHUNK).await;
    assert!(
        second.contains(SSE_SECOND_CHUNK),
        "the second chunk must arrive once the tls handler is released, got: {second:?}"
    );
    control.shutdown().await;
}

#[tokio::test]
async fn http1_sse_response_carries_the_event_stream_content_type() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, None)
            .await
            .expect("http1 plaintext server must start");
    let _lease: GateLease = gate_lease(GateSlot::SseHttp1).await;
    let mut stream: TcpStream = connect_tcp(port).await.expect("socket must connect");
    let request: Vec<u8> = http1_close_request("GET", SSE_ROUTE_PATH);
    write_bounded(&mut stream, &request)
        .await
        .expect("sse request must be written");
    let head: String = http1_head_until(&mut stream, HTTP_DOUBLE_BR).await;
    assert!(
        head.to_lowercase().contains(TEXT_EVENT_STREAM),
        "the sse route must send an event-stream content type, got: {head}"
    );
    route_gate(GateSlot::SseHttp1).release_sse();
    let completed: String = http1_head_until(&mut stream, SSE_SECOND_CHUNK).await;
    assert!(
        completed.contains(SSE_SECOND_CHUNK),
        "the sse response must run to its end once released, got: {completed:?}"
    );
    control.shutdown().await;
}

#[tokio::test]
async fn http1_plaintext_repeats_a_duplicate_response_cookie() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, None)
            .await
            .expect("http1 plaintext server must start");
    let request: Vec<u8> = http1_close_request("GET", COOKIE_ROUTE_PATH);
    let raw: Vec<u8> = raw_http1_exchange(port, &request)
        .await
        .expect("cookie exchange must complete");
    let head: String = response_head(&raw);
    let cookies: usize = head.to_lowercase().matches(SET_COOKIE).count();
    assert_eq!(
        cookies, 2,
        "both set-cookie headers must survive, got: {head}"
    );
    assert!(
        head.contains("dup=one") && head.contains("dup2=two"),
        "both cookie values must be present, got: {head}"
    );
    control.shutdown().await;
}

#[tokio::test]
async fn http1_plaintext_answers_four_pipelined_requests_on_one_connection() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, None)
            .await
            .expect("http1 plaintext server must start");
    sleep(Duration::from_millis(SERVER_BOOT_MILLIS)).await;
    let mut stream: TcpStream = connect_tcp(port).await.expect("socket must connect");
    let mut batch: Vec<u8> = Vec::new();
    for _ in 0..4 {
        batch.extend_from_slice(&http1_request("GET", VERSION_ROUTE_PATH, &[]));
    }
    write_bounded(&mut stream, &batch)
        .await
        .expect("pipelined batch must be written");
    let mut received: Vec<u8> = Vec::new();
    let deadline: Instant = Instant::now() + Duration::from_secs(REQUEST_TIMEOUT_SECS);
    let mut chunk: [u8; 16 * 1024] = [0; 16 * 1024];
    while Instant::now() < deadline && count_http1_responses(&received) < 4 {
        match stream.read(&mut chunk).await {
            Ok(0) => break,
            Ok(read) => received.extend_from_slice(&chunk[..read]),
            Err(_) => break,
        }
    }
    assert_eq!(
        count_http1_responses(&received),
        4,
        "every pipelined request must be answered on the same connection"
    );
    control.shutdown().await;
}

#[tokio::test]
async fn http1_plaintext_shutdown_releases_the_bound_tcp_port() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, None)
            .await
            .expect("http1 plaintext server must start");
    assert_wait_pending_then_complete(&control).await;
    let rebound: io::Result<TcpListener> = TcpListener::bind(format!("{LOOPBACK_HOST}:{port}"));
    assert!(
        rebound.is_ok(),
        "port {port} must be free again after shutdown and wait"
    );
}

#[tokio::test]
async fn http1_tls_shutdown_releases_the_bound_tcp_port() {
    let (control, port): (ServerControlHook, u16) =
        start_protocol_server(ServerProtocol::Http1, Some(trusted_tls_config()))
            .await
            .expect("http1 tls server must start");
    assert_wait_pending_then_complete(&control).await;
    let rebound: io::Result<TcpListener> = TcpListener::bind(format!("{LOOPBACK_HOST}:{port}"));
    assert!(
        rebound.is_ok(),
        "port {port} must be free again after a tls shutdown and wait"
    );
}