1pub mod adapter;
63pub mod builder;
64pub mod error;
65pub mod handler;
66pub mod prelude;
67pub mod server;
68
69#[cfg(feature = "cors")]
70pub mod cors;
71
72#[cfg(feature = "sse")]
73pub mod streaming;
74
75pub use builder::LambdaMcpServerBuilder;
78pub use error::{LambdaError, Result};
80pub use handler::LambdaMcpHandler;
82pub use server::LambdaMcpServer;
84
85#[cfg(feature = "cors")]
86pub use cors::{CorsConfig, create_preflight_response, inject_cors_headers};
87
88#[derive(Debug)]
93enum RuntimeEventClassification {
94 ApiGatewayEvent(Box<lambda_http::request::LambdaRequest>),
100 StreamingCompletion,
102 UnrecognizedEvent,
104}
105
106fn classify_runtime_event(payload: serde_json::Value) -> RuntimeEventClassification {
131 if let Ok(request) =
133 serde_json::from_value::<lambda_http::request::LambdaRequest>(payload.clone())
134 {
135 return RuntimeEventClassification::ApiGatewayEvent(Box::new(request));
136 }
137
138 if payload.get("invokeCompletionStatus").is_some() {
140 return RuntimeEventClassification::StreamingCompletion;
141 }
142
143 RuntimeEventClassification::UnrecognizedEvent
145}
146
147type StreamBody = http_body_util::combinators::UnsyncBoxBody<bytes::Bytes, hyper::Error>;
148type StreamResult =
149 lambda_runtime::StreamResponse<http_body_util::BodyDataStream<EnsureOneFrame<StreamBody>>>;
150
151struct HandleResult {
154 response: StreamResult,
155 event_type: &'static str,
158}
159
160async fn handle_runtime_payload<F, Fut>(
167 payload: serde_json::Value,
168 context: lambda_runtime::Context,
169 dispatch: F,
170) -> std::result::Result<HandleResult, lambda_http::Error>
171where
172 F: FnOnce(lambda_http::Request) -> Fut,
173 Fut: std::future::Future<
174 Output = std::result::Result<http::Response<StreamBody>, lambda_http::Error>,
175 >,
176{
177 match classify_runtime_event(payload) {
178 RuntimeEventClassification::ApiGatewayEvent(lambda_request) => {
179 use lambda_http::RequestExt;
180 let request: lambda_http::Request = (*lambda_request).into();
181 let request = request.with_lambda_context(context);
182 let response = dispatch(request).await?;
183 Ok(HandleResult {
184 response: into_lambda_stream_response(response),
185 event_type: "api_gateway_event",
186 })
187 }
188 RuntimeEventClassification::StreamingCompletion => Ok(HandleResult {
189 response: into_lambda_stream_response(empty_streaming_response()),
190 event_type: "streaming_completion",
191 }),
192 RuntimeEventClassification::UnrecognizedEvent => Ok(HandleResult {
193 response: into_lambda_stream_response(empty_streaming_response()),
194 event_type: "unrecognized_lambda_payload",
195 }),
196 }
197}
198
199fn event_log_level(event_type: &str) -> Option<tracing::Level> {
205 match event_type {
206 "streaming_completion" => Some(tracing::Level::DEBUG),
207 "unrecognized_lambda_payload" => Some(tracing::Level::WARN),
208 _ => None,
209 }
210}
211
212pub async fn run_streaming(
243 handler: LambdaMcpHandler,
244) -> std::result::Result<(), lambda_http::Error> {
245 use lambda_runtime::{LambdaEvent, service_fn};
246
247 lambda_runtime::run(service_fn(move |event: LambdaEvent<serde_json::Value>| {
248 let handler = handler.clone();
249 async move {
250 let result = handle_runtime_payload(event.payload, event.context, |req| {
251 handler.handle_streaming(req)
252 })
253 .await?;
254
255 match event_log_level(result.event_type) {
256 Some(level) if level == tracing::Level::WARN => {
257 tracing::warn!(
258 event_type = result.event_type,
259 "Received unrecognized Lambda invocation payload"
260 );
261 }
262 Some(_) => {
263 tracing::debug!(
264 event_type = result.event_type,
265 "Acknowledging streaming completion"
266 );
267 }
268 None => {}
269 }
270
271 Ok::<_, lambda_http::Error>(result.response)
272 }
273 }))
274 .await
275}
276
277pub async fn run_streaming_with<F, Fut>(dispatch: F) -> std::result::Result<(), lambda_http::Error>
299where
300 F: Fn(lambda_http::Request) -> Fut + Clone + Send + 'static,
301 Fut: std::future::Future<
302 Output = std::result::Result<http::Response<StreamBody>, lambda_http::Error>,
303 > + Send,
304{
305 use lambda_runtime::{LambdaEvent, service_fn};
306
307 lambda_runtime::run(service_fn(move |event: LambdaEvent<serde_json::Value>| {
308 let dispatch = dispatch.clone();
309 async move {
310 let result = handle_runtime_payload(event.payload, event.context, dispatch).await?;
311
312 match event_log_level(result.event_type) {
313 Some(level) if level == tracing::Level::WARN => {
314 tracing::warn!(
315 event_type = result.event_type,
316 "Received unrecognized Lambda invocation payload"
317 );
318 }
319 Some(_) => {
320 tracing::debug!(
321 event_type = result.event_type,
322 "Acknowledging streaming completion"
323 );
324 }
325 None => {}
326 }
327
328 Ok::<_, lambda_http::Error>(result.response)
329 }
330 }))
331 .await
332}
333
334struct EnsureOneFrame<B> {
352 inner: B,
353 first_frame_state: FirstFrameState,
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359enum FirstFrameState {
360 Initial,
363 Done,
366 FallbackEmitted,
369}
370
371impl<B> http_body::Body for EnsureOneFrame<B>
372where
373 B: http_body::Body<Data = bytes::Bytes> + Unpin,
374{
375 type Data = bytes::Bytes;
376 type Error = B::Error;
377
378 fn poll_frame(
379 mut self: std::pin::Pin<&mut Self>,
380 cx: &mut std::task::Context<'_>,
381 ) -> std::task::Poll<Option<std::result::Result<http_body::Frame<Self::Data>, Self::Error>>>
382 {
383 use std::task::Poll;
384 match self.first_frame_state {
385 FirstFrameState::FallbackEmitted => Poll::Ready(None),
386 FirstFrameState::Done => std::pin::Pin::new(&mut self.inner).poll_frame(cx),
387 FirstFrameState::Initial => {
388 match std::pin::Pin::new(&mut self.inner).poll_frame(cx) {
389 Poll::Pending => Poll::Pending,
390 Poll::Ready(Some(Ok(frame))) => {
391 self.first_frame_state = FirstFrameState::Done;
392 Poll::Ready(Some(Ok(frame)))
393 }
394 Poll::Ready(Some(Err(e))) => {
395 self.first_frame_state = FirstFrameState::Done;
400 Poll::Ready(Some(Err(e)))
401 }
402 Poll::Ready(None) => {
403 self.first_frame_state = FirstFrameState::FallbackEmitted;
404 Poll::Ready(Some(Ok(http_body::Frame::data(bytes::Bytes::new()))))
405 }
406 }
407 }
408 }
409 }
410
411 fn is_end_stream(&self) -> bool {
412 match self.first_frame_state {
413 FirstFrameState::Initial => false,
414 FirstFrameState::Done => self.inner.is_end_stream(),
415 FirstFrameState::FallbackEmitted => true,
416 }
417 }
418
419 fn size_hint(&self) -> http_body::SizeHint {
420 self.inner.size_hint()
425 }
426}
427
428fn into_lambda_stream_response<B>(
438 response: http::Response<B>,
439) -> lambda_runtime::StreamResponse<http_body_util::BodyDataStream<EnsureOneFrame<B>>>
440where
441 B: http_body::Body<Data = bytes::Bytes> + Unpin + Send + 'static,
442{
443 let (parts, body) = response.into_parts();
444 let mut headers = parts.headers;
445
446 let cookies = headers
448 .get_all(http::header::SET_COOKIE)
449 .iter()
450 .map(|c| String::from_utf8_lossy(c.as_bytes()).to_string())
451 .collect::<Vec<_>>();
452 headers.remove(http::header::SET_COOKIE);
453
454 let body = EnsureOneFrame {
455 inner: body,
456 first_frame_state: FirstFrameState::Initial,
457 };
458
459 lambda_runtime::StreamResponse {
460 metadata_prelude: lambda_runtime::MetadataPrelude {
461 headers,
462 status_code: parts.status,
463 cookies,
464 },
465 stream: http_body_util::BodyDataStream::new(body),
466 }
467}
468
469fn empty_streaming_response()
471-> http::Response<http_body_util::combinators::UnsyncBoxBody<bytes::Bytes, hyper::Error>> {
472 use http_body_util::{BodyExt, Full};
473 let body = Full::new(bytes::Bytes::new())
474 .map_err(|e: std::convert::Infallible| match e {})
475 .boxed_unsync();
476 http::Response::builder().status(200).body(body).unwrap()
477}
478
479#[cfg(test)]
480mod streaming_completion_tests {
481 use super::*;
482 use serde_json::json;
483
484 fn load_fixture(name: &str) -> serde_json::Value {
487 let json_str = match name {
488 "apigw_v1" => include_str!("fixtures/apigw_v1_proxy_event.json"),
489 "apigw_v2" => include_str!("fixtures/apigw_v2_http_api_event.json"),
490 "completion_success" => include_str!("fixtures/streaming_completion_success.json"),
491 "completion_failure" => include_str!("fixtures/streaming_completion_failure.json"),
492 "completion_extra" => include_str!("fixtures/streaming_completion_extra_fields.json"),
493 "completion_api_like" => {
494 include_str!("fixtures/completion_with_api_like_fields.json")
495 }
496 other => panic!("Unknown fixture: {other}"),
497 };
498 serde_json::from_str(json_str).unwrap_or_else(|e| panic!("Bad fixture {name}: {e}"))
499 }
500
501 #[test]
504 fn test_classify_api_gateway_v1_event() {
505 let payload = load_fixture("apigw_v1");
506 assert!(
507 matches!(
508 classify_runtime_event(payload),
509 RuntimeEventClassification::ApiGatewayEvent(_)
510 ),
511 "API Gateway v1 proxy event must classify as ApiGatewayEvent"
512 );
513 }
514
515 #[test]
516 fn test_classify_api_gateway_v2_event() {
517 let payload = load_fixture("apigw_v2");
518 assert!(
519 matches!(
520 classify_runtime_event(payload),
521 RuntimeEventClassification::ApiGatewayEvent(_)
522 ),
523 "API Gateway v2 HTTP API event must classify as ApiGatewayEvent"
524 );
525 }
526
527 #[test]
530 fn test_classify_streaming_completion() {
531 let payload = load_fixture("completion_success");
532 assert!(matches!(
533 classify_runtime_event(payload),
534 RuntimeEventClassification::StreamingCompletion
535 ));
536 }
537
538 #[test]
539 fn test_classify_completion_failure_status() {
540 let payload = load_fixture("completion_failure");
541 assert!(matches!(
542 classify_runtime_event(payload),
543 RuntimeEventClassification::StreamingCompletion
544 ));
545 }
546
547 #[test]
548 fn test_classify_completion_extra_fields() {
549 let payload = load_fixture("completion_extra");
550 assert!(matches!(
551 classify_runtime_event(payload),
552 RuntimeEventClassification::StreamingCompletion
553 ));
554 }
555
556 #[test]
559 fn test_classify_completion_with_api_like_fields() {
560 let payload = load_fixture("completion_api_like");
568 assert!(matches!(
569 classify_runtime_event(payload),
570 RuntimeEventClassification::StreamingCompletion
571 ));
572 }
573
574 #[test]
577 fn test_classify_empty_object() {
578 assert!(matches!(
579 classify_runtime_event(json!({})),
580 RuntimeEventClassification::UnrecognizedEvent
581 ));
582 }
583
584 #[test]
585 fn test_classify_random_object() {
586 assert!(matches!(
587 classify_runtime_event(json!({"foo": "bar", "baz": 123})),
588 RuntimeEventClassification::UnrecognizedEvent
589 ));
590 }
591
592 #[test]
593 fn test_classify_null_payload() {
594 assert!(matches!(
595 classify_runtime_event(json!(null)),
596 RuntimeEventClassification::UnrecognizedEvent
597 ));
598 }
599
600 #[test]
601 fn test_classify_string_payload() {
602 assert!(matches!(
603 classify_runtime_event(json!("hello")),
604 RuntimeEventClassification::UnrecognizedEvent
605 ));
606 }
607
608 #[test]
609 fn test_classify_array_payload() {
610 assert!(matches!(
611 classify_runtime_event(json!([1, 2, 3])),
612 RuntimeEventClassification::UnrecognizedEvent
613 ));
614 }
615
616 #[test]
617 fn test_classify_nested_invoke_status() {
618 let payload = json!({
620 "data": {"invokeCompletionStatus": "Success"}
621 });
622 assert!(matches!(
623 classify_runtime_event(payload),
624 RuntimeEventClassification::UnrecognizedEvent
625 ));
626 }
627
628 #[test]
631 fn test_classify_never_panics_on_arbitrary_json() {
632 let payloads = vec![
634 json!(null),
635 json!(true),
636 json!(false),
637 json!(42),
638 json!(-1.5),
639 json!(""),
640 json!("some string"),
641 json!([]),
642 json!([1, "two", null, false]),
643 json!({}),
644 json!({"a": 1}),
645 json!({"requestContext": null}),
646 json!({"requestContext": "not-an-object"}),
647 json!({"httpMethod": "POST"}),
648 json!({"version": "2.0"}),
649 json!({"version": "2.0", "routeKey": "GET /"}),
650 json!({"resource": "/", "httpMethod": "GET"}),
651 json!({"deeply": {"nested": {"invokeCompletionStatus": "Success"}}}),
652 serde_json::Value::Object((0..100).map(|i| (format!("key_{i}"), json!(i))).collect()),
654 ];
655
656 for payload in payloads {
657 let _result = classify_runtime_event(payload);
658 }
659 }
660
661 #[test]
662 fn test_classify_invoke_completion_status_always_wins() {
663 let payloads = vec![
666 json!({"invokeCompletionStatus": "Success"}),
667 json!({"invokeCompletionStatus": "Failure"}),
668 json!({"invokeCompletionStatus": "Unknown"}),
669 json!({"invokeCompletionStatus": 42}),
670 json!({"invokeCompletionStatus": null}),
671 json!({"invokeCompletionStatus": "Success", "requestId": "abc-123"}),
672 json!({"invokeCompletionStatus": "Success", "extra": "field", "nested": {"a": 1}}),
673 ];
674
675 for payload in payloads {
676 let result = classify_runtime_event(payload.clone());
677 assert!(
678 matches!(result, RuntimeEventClassification::StreamingCompletion),
679 "Payload with top-level invokeCompletionStatus must be StreamingCompletion: {payload}"
680 );
681 }
682 }
683
684 #[test]
687 fn test_unrecognized_logs_at_warn_level() {
688 assert_eq!(
689 event_log_level("unrecognized_lambda_payload"),
690 Some(tracing::Level::WARN)
691 );
692 }
693
694 #[test]
695 fn test_completion_logs_at_debug_level() {
696 assert_eq!(
697 event_log_level("streaming_completion"),
698 Some(tracing::Level::DEBUG)
699 );
700 }
701
702 #[test]
703 fn test_api_gateway_has_no_extra_logging() {
704 assert_eq!(event_log_level("api_gateway_event"), None);
705 }
706
707 #[tokio::test]
710 async fn test_handle_completion_does_not_dispatch() {
711 let dispatched = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
712 let dispatched_clone = dispatched.clone();
713
714 let result = handle_runtime_payload(
715 load_fixture("completion_success"),
716 lambda_runtime::Context::default(),
717 |_req| {
718 let d = dispatched_clone.clone();
719 async move {
720 d.store(true, std::sync::atomic::Ordering::SeqCst);
721 Ok(empty_streaming_response())
722 }
723 },
724 )
725 .await
726 .expect("handle should succeed");
727
728 assert!(
729 !dispatched.load(std::sync::atomic::Ordering::SeqCst),
730 "Completion events must not dispatch to handler"
731 );
732 assert_eq!(result.event_type, "streaming_completion");
733 assert_eq!(result.response.metadata_prelude.status_code, 200);
734 }
735
736 #[tokio::test]
737 async fn test_handle_unrecognized_does_not_dispatch() {
738 let dispatched = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
739 let dispatched_clone = dispatched.clone();
740
741 let result = handle_runtime_payload(
742 json!({"foo": "bar"}),
743 lambda_runtime::Context::default(),
744 |_req| {
745 let d = dispatched_clone.clone();
746 async move {
747 d.store(true, std::sync::atomic::Ordering::SeqCst);
748 Ok(empty_streaming_response())
749 }
750 },
751 )
752 .await
753 .expect("handle should succeed");
754
755 assert!(
756 !dispatched.load(std::sync::atomic::Ordering::SeqCst),
757 "Unrecognized events must not dispatch to handler"
758 );
759 assert_eq!(result.event_type, "unrecognized_lambda_payload");
760 assert_eq!(result.response.metadata_prelude.status_code, 200);
761 }
762
763 #[tokio::test]
764 async fn test_handle_apigw_v1_dispatches() {
765 let dispatched = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
766 let dispatched_clone = dispatched.clone();
767
768 let result = handle_runtime_payload(
769 load_fixture("apigw_v1"),
770 lambda_runtime::Context::default(),
771 |_req| {
772 let d = dispatched_clone.clone();
773 async move {
774 d.store(true, std::sync::atomic::Ordering::SeqCst);
775 Ok(empty_streaming_response())
776 }
777 },
778 )
779 .await
780 .expect("handle should succeed");
781
782 assert!(
783 dispatched.load(std::sync::atomic::Ordering::SeqCst),
784 "API Gateway v1 events must dispatch to handler"
785 );
786 assert_eq!(result.event_type, "api_gateway_event");
787 }
788
789 #[tokio::test]
790 async fn test_handle_apigw_v2_dispatches() {
791 let dispatched = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
792 let dispatched_clone = dispatched.clone();
793
794 let result = handle_runtime_payload(
795 load_fixture("apigw_v2"),
796 lambda_runtime::Context::default(),
797 |_req| {
798 let d = dispatched_clone.clone();
799 async move {
800 d.store(true, std::sync::atomic::Ordering::SeqCst);
801 Ok(empty_streaming_response())
802 }
803 },
804 )
805 .await
806 .expect("handle should succeed");
807
808 assert!(
809 dispatched.load(std::sync::atomic::Ordering::SeqCst),
810 "API Gateway v2 events must dispatch to handler"
811 );
812 assert_eq!(result.event_type, "api_gateway_event");
813 }
814
815 #[tokio::test]
816 async fn test_handle_unrecognized_surfaces_distinct_event_type() {
817 let result = handle_runtime_payload(
818 json!({"unknown": true}),
819 lambda_runtime::Context::default(),
820 |_req| async { Ok(empty_streaming_response()) },
821 )
822 .await
823 .expect("handle should succeed");
824
825 assert_eq!(result.event_type, "unrecognized_lambda_payload");
826 }
827
828 #[test]
831 fn test_empty_streaming_response() {
832 let resp = empty_streaming_response();
833 assert_eq!(resp.status(), 200);
834 }
835
836 #[test]
837 fn test_into_lambda_stream_response_preserves_metadata() {
838 use http_body_util::{BodyExt, Full};
839
840 let response = http::Response::builder()
841 .status(401)
842 .header("WWW-Authenticate", "Bearer realm=\"mcp\"")
843 .header("X-Custom", "test")
844 .body(
845 Full::new(bytes::Bytes::from("Unauthorized"))
846 .map_err(|e: std::convert::Infallible| match e {})
847 .boxed_unsync(),
848 )
849 .unwrap();
850
851 let stream_resp = into_lambda_stream_response(response);
852 assert_eq!(stream_resp.metadata_prelude.status_code, 401);
853 assert_eq!(
854 stream_resp
855 .metadata_prelude
856 .headers
857 .get("WWW-Authenticate")
858 .unwrap(),
859 "Bearer realm=\"mcp\""
860 );
861 assert_eq!(
862 stream_resp
863 .metadata_prelude
864 .headers
865 .get("X-Custom")
866 .unwrap(),
867 "test"
868 );
869 }
870
871 #[test]
872 fn test_into_lambda_stream_response_extracts_cookies() {
873 use http_body_util::{BodyExt, Full};
874
875 let response = http::Response::builder()
876 .status(200)
877 .header("Set-Cookie", "session=abc; Path=/")
878 .header("Set-Cookie", "theme=dark")
879 .body(
880 Full::new(bytes::Bytes::new())
881 .map_err(|e: std::convert::Infallible| match e {})
882 .boxed_unsync(),
883 )
884 .unwrap();
885
886 let stream_resp = into_lambda_stream_response(response);
887 assert_eq!(stream_resp.metadata_prelude.cookies.len(), 2);
888 assert!(
889 stream_resp
890 .metadata_prelude
891 .cookies
892 .contains(&"session=abc; Path=/".to_string())
893 );
894 assert!(
895 stream_resp
896 .metadata_prelude
897 .cookies
898 .contains(&"theme=dark".to_string())
899 );
900 assert!(
901 stream_resp
902 .metadata_prelude
903 .headers
904 .get("Set-Cookie")
905 .is_none()
906 );
907 }
908
909 #[tokio::test]
912 async fn test_run_streaming_with_dispatches_apigw_events() {
913 let dispatched = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
914 let dispatched_clone = dispatched.clone();
915
916 let dispatch = move |_req: lambda_http::Request| {
917 let d = dispatched_clone.clone();
918 async move {
919 d.store(true, std::sync::atomic::Ordering::SeqCst);
920 Ok(empty_streaming_response())
921 }
922 };
923
924 let result = handle_runtime_payload(
925 load_fixture("apigw_v1"),
926 lambda_runtime::Context::default(),
927 dispatch,
928 )
929 .await
930 .expect("handle should succeed");
931
932 assert!(
933 dispatched.load(std::sync::atomic::Ordering::SeqCst),
934 "run_streaming_with dispatch must be called for API Gateway events"
935 );
936 assert_eq!(result.event_type, "api_gateway_event");
937 }
938
939 async fn drain_stream<S, B>(
965 stream: lambda_runtime::StreamResponse<S>,
966 ) -> (u16, Vec<bytes::Bytes>)
967 where
968 S: futures::Stream<Item = std::result::Result<bytes::Bytes, B>> + Unpin,
969 {
970 use futures::StreamExt;
971 let status = stream.metadata_prelude.status_code.as_u16();
972 let mut frames = Vec::new();
973 let mut s = stream.stream;
974 while let Some(item) = s.next().await {
975 frames.push(item.unwrap_or_else(|_| bytes::Bytes::new()));
976 }
977 (status, frames)
978 }
979
980 #[tokio::test]
984 async fn test_into_lambda_stream_response_empty_body_yields_data_frame() {
985 use http_body_util::{BodyExt, Empty};
986
987 let body = Empty::<bytes::Bytes>::new()
988 .map_err(|_: std::convert::Infallible| -> hyper::Error { unreachable!() })
989 .boxed_unsync();
990 let response = http::Response::builder()
991 .status(204)
992 .header("Access-Control-Allow-Origin", "*")
993 .header("Access-Control-Allow-Methods", "GET, OPTIONS")
994 .body(body)
995 .unwrap();
996
997 let stream = into_lambda_stream_response(response);
998 let (status, frames) = drain_stream(stream).await;
999
1000 assert_eq!(status, 204);
1001 assert!(
1002 !frames.is_empty(),
1003 "empty-body streaming response must yield at least one data frame \
1004 (got zero frames — Runtime API will see IncompleteMessage); \
1005 frames: {:?}",
1006 frames,
1007 );
1008 }
1009
1010 #[tokio::test]
1017 async fn test_run_streaming_with_empty_body_dispatch_yields_data_frame() {
1018 let dispatch = |_req: lambda_http::Request| async move {
1019 use http_body_util::{BodyExt, Empty};
1020 let body = Empty::<bytes::Bytes>::new()
1021 .map_err(|_: std::convert::Infallible| -> hyper::Error { unreachable!() })
1022 .boxed_unsync();
1023 let resp = http::Response::builder()
1024 .status(204)
1025 .header("Access-Control-Allow-Origin", "https://client.example.test")
1026 .header("Access-Control-Allow-Methods", "GET, OPTIONS")
1027 .header("Access-Control-Allow-Headers", "Content-Type")
1028 .body(body)
1029 .unwrap();
1030 Ok::<_, lambda_http::Error>(resp)
1031 };
1032
1033 let result = handle_runtime_payload(
1034 load_fixture("apigw_v2"),
1035 lambda_runtime::Context::default(),
1036 dispatch,
1037 )
1038 .await
1039 .expect("handle_runtime_payload must succeed");
1040
1041 assert_eq!(result.event_type, "api_gateway_event");
1042 let (status, frames) = drain_stream(result.response).await;
1043 assert_eq!(status, 204);
1044 assert!(
1045 !frames.is_empty(),
1046 "run_streaming_with dispatch returning Empty::new() body must yield \
1047 at least one data frame through the envelope; got zero — this is \
1048 the production .well-known OPTIONS 502 / IncompleteMessage path; \
1049 frames: {:?}",
1050 frames,
1051 );
1052 }
1053
1054 #[tokio::test]
1057 async fn test_into_lambda_stream_response_non_empty_body_preserves_bytes() {
1058 use http_body_util::{BodyExt, Full};
1059
1060 let payload = bytes::Bytes::from_static(b"hello");
1061 let body = Full::new(payload.clone())
1062 .map_err(|_: std::convert::Infallible| -> hyper::Error { unreachable!() })
1063 .boxed_unsync();
1064 let response = http::Response::builder().status(200).body(body).unwrap();
1065
1066 let stream = into_lambda_stream_response(response);
1067 let (status, frames) = drain_stream(stream).await;
1068 assert_eq!(status, 200);
1069 let joined: Vec<u8> = frames.iter().flat_map(|f| f.iter().copied()).collect();
1070 assert_eq!(
1071 joined,
1072 payload.to_vec(),
1073 "non-empty body must round-trip its bytes through the envelope",
1074 );
1075 }
1076
1077 #[tokio::test]
1078 async fn test_run_streaming_with_acks_completion_without_dispatch() {
1079 let dispatched = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1080 let dispatched_clone = dispatched.clone();
1081
1082 let dispatch = move |_req: lambda_http::Request| {
1083 let d = dispatched_clone.clone();
1084 async move {
1085 d.store(true, std::sync::atomic::Ordering::SeqCst);
1086 Ok(empty_streaming_response())
1087 }
1088 };
1089
1090 let result = handle_runtime_payload(
1091 load_fixture("completion_success"),
1092 lambda_runtime::Context::default(),
1093 dispatch,
1094 )
1095 .await
1096 .expect("handle should succeed");
1097
1098 assert!(
1099 !dispatched.load(std::sync::atomic::Ordering::SeqCst),
1100 "run_streaming_with dispatch must NOT be called for completion events"
1101 );
1102 assert_eq!(result.event_type, "streaming_completion");
1103 assert_eq!(result.response.metadata_prelude.status_code, 200);
1104 }
1105}