@@ -657,6 +657,83 @@ describe('createHonoApp', () => {
657657 ) ;
658658 } ) ;
659659
660+ it ( 'POST /api/v1/data/account parses JSON body through outer→inner delegation' , async ( ) => {
661+ const outerApp = createVercelApp ( ) ;
662+ const body = { name : 'Acme' } ;
663+
664+ const res = await outerApp . request ( '/api/v1/data/account' , {
665+ method : 'POST' ,
666+ headers : { 'Content-Type' : 'application/json' } ,
667+ body : JSON . stringify ( body ) ,
668+ } ) ;
669+ expect ( res . status ) . toBe ( 200 ) ;
670+ expect ( mockDispatcher . dispatch ) . toHaveBeenCalledWith (
671+ 'POST' ,
672+ '/data/account' ,
673+ body ,
674+ expect . any ( Object ) ,
675+ expect . objectContaining ( { request : expect . anything ( ) } ) ,
676+ '/api/v1' ,
677+ ) ;
678+ } ) ;
679+
680+ it ( 'PUT /api/v1/data/account parses JSON body through outer→inner delegation' , async ( ) => {
681+ const outerApp = createVercelApp ( ) ;
682+ const body = { name : 'Updated' } ;
683+
684+ const res = await outerApp . request ( '/api/v1/data/account' , {
685+ method : 'PUT' ,
686+ headers : { 'Content-Type' : 'application/json' } ,
687+ body : JSON . stringify ( body ) ,
688+ } ) ;
689+ expect ( res . status ) . toBe ( 200 ) ;
690+ expect ( mockDispatcher . dispatch ) . toHaveBeenCalledWith (
691+ 'PUT' ,
692+ '/data/account' ,
693+ body ,
694+ expect . any ( Object ) ,
695+ expect . objectContaining ( { request : expect . anything ( ) } ) ,
696+ '/api/v1' ,
697+ ) ;
698+ } ) ;
699+
700+ it ( 'PATCH /api/v1/data/account parses JSON body through outer→inner delegation' , async ( ) => {
701+ const outerApp = createVercelApp ( ) ;
702+ const body = { name : 'Patched' } ;
703+
704+ const res = await outerApp . request ( '/api/v1/data/account' , {
705+ method : 'PATCH' ,
706+ headers : { 'Content-Type' : 'application/json' } ,
707+ body : JSON . stringify ( body ) ,
708+ } ) ;
709+ expect ( res . status ) . toBe ( 200 ) ;
710+ expect ( mockDispatcher . dispatch ) . toHaveBeenCalledWith (
711+ 'PATCH' ,
712+ '/data/account' ,
713+ body ,
714+ expect . any ( Object ) ,
715+ expect . objectContaining ( { request : expect . anything ( ) } ) ,
716+ '/api/v1' ,
717+ ) ;
718+ } ) ;
719+
720+ it ( 'DELETE /api/v1/data/account routes through outer→inner delegation' , async ( ) => {
721+ const outerApp = createVercelApp ( ) ;
722+
723+ const res = await outerApp . request ( '/api/v1/data/account' , {
724+ method : 'DELETE' ,
725+ } ) ;
726+ expect ( res . status ) . toBe ( 200 ) ;
727+ expect ( mockDispatcher . dispatch ) . toHaveBeenCalledWith (
728+ 'DELETE' ,
729+ '/data/account' ,
730+ undefined ,
731+ expect . any ( Object ) ,
732+ expect . objectContaining ( { request : expect . anything ( ) } ) ,
733+ '/api/v1' ,
734+ ) ;
735+ } ) ;
736+
660737 it ( 'returns 500 with error details when inner app throws' , async ( ) => {
661738 const outerApp = new Hono ( ) ;
662739
@@ -680,6 +757,153 @@ describe('createHonoApp', () => {
680757 } ) ;
681758 } ) ;
682759
760+ describe ( 'Body-safe Vercel delegation (buffered body forwarding)' , ( ) => {
761+ /**
762+ * Validates the body-safe delegation pattern used in
763+ * `apps/studio/server/index.ts` where the outer handler buffers
764+ * POST/PUT/PATCH bodies and creates a fresh `Request` for the inner app.
765+ * This avoids @hono/node-server's lazy body materialisation which can
766+ * hang on Vercel when the IncomingMessage stream state has changed.
767+ */
768+ function createBodySafeVercelApp ( ) {
769+ const innerApp = createHonoApp ( { kernel : mockKernel , prefix : '/api/v1' } ) ;
770+ const outerApp = new Hono ( ) ;
771+
772+ outerApp . all ( '*' , async ( c ) => {
773+ const method = c . req . method ;
774+
775+ // GET/HEAD have no body — pass through directly
776+ if ( method === 'GET' || method === 'HEAD' ) {
777+ return innerApp . fetch ( c . req . raw ) ;
778+ }
779+
780+ // Buffer body and create a fresh Request
781+ const rawReq = c . req . raw ;
782+ const body = await rawReq . arrayBuffer ( ) ;
783+ const forwarded = new Request ( rawReq . url , {
784+ method,
785+ headers : rawReq . headers ,
786+ body,
787+ } ) ;
788+ return innerApp . fetch ( forwarded ) ;
789+ } ) ;
790+
791+ return outerApp ;
792+ }
793+
794+ it ( 'GET requests work without body buffering' , async ( ) => {
795+ const outerApp = createBodySafeVercelApp ( ) ;
796+
797+ const res = await outerApp . request ( '/api/v1/data/account' ) ;
798+ expect ( res . status ) . toBe ( 200 ) ;
799+ expect ( mockDispatcher . dispatch ) . toHaveBeenCalledWith (
800+ 'GET' ,
801+ '/data/account' ,
802+ undefined ,
803+ expect . any ( Object ) ,
804+ expect . objectContaining ( { request : expect . anything ( ) } ) ,
805+ '/api/v1' ,
806+ ) ;
807+ } ) ;
808+
809+ it ( 'POST body is forwarded correctly via buffered delegation' , async ( ) => {
810+ const outerApp = createBodySafeVercelApp ( ) ;
811+ const body = { name : 'Acme Corp' } ;
812+
813+ const res = await outerApp . request ( '/api/v1/data/account' , {
814+ method : 'POST' ,
815+ headers : { 'Content-Type' : 'application/json' } ,
816+ body : JSON . stringify ( body ) ,
817+ } ) ;
818+ expect ( res . status ) . toBe ( 200 ) ;
819+ expect ( mockDispatcher . dispatch ) . toHaveBeenCalledWith (
820+ 'POST' ,
821+ '/data/account' ,
822+ body ,
823+ expect . any ( Object ) ,
824+ expect . objectContaining ( { request : expect . anything ( ) } ) ,
825+ '/api/v1' ,
826+ ) ;
827+ } ) ;
828+
829+ it ( 'PUT body is forwarded correctly via buffered delegation' , async ( ) => {
830+ const outerApp = createBodySafeVercelApp ( ) ;
831+ const body = { name : 'Updated Corp' } ;
832+
833+ const res = await outerApp . request ( '/api/v1/data/account' , {
834+ method : 'PUT' ,
835+ headers : { 'Content-Type' : 'application/json' } ,
836+ body : JSON . stringify ( body ) ,
837+ } ) ;
838+ expect ( res . status ) . toBe ( 200 ) ;
839+ expect ( mockDispatcher . dispatch ) . toHaveBeenCalledWith (
840+ 'PUT' ,
841+ '/data/account' ,
842+ body ,
843+ expect . any ( Object ) ,
844+ expect . objectContaining ( { request : expect . anything ( ) } ) ,
845+ '/api/v1' ,
846+ ) ;
847+ } ) ;
848+
849+ it ( 'PATCH body is forwarded correctly via buffered delegation' , async ( ) => {
850+ const outerApp = createBodySafeVercelApp ( ) ;
851+ const body = { status : 'active' } ;
852+
853+ const res = await outerApp . request ( '/api/v1/data/account' , {
854+ method : 'PATCH' ,
855+ headers : { 'Content-Type' : 'application/json' } ,
856+ body : JSON . stringify ( body ) ,
857+ } ) ;
858+ expect ( res . status ) . toBe ( 200 ) ;
859+ expect ( mockDispatcher . dispatch ) . toHaveBeenCalledWith (
860+ 'PATCH' ,
861+ '/data/account' ,
862+ body ,
863+ expect . any ( Object ) ,
864+ expect . objectContaining ( { request : expect . anything ( ) } ) ,
865+ '/api/v1' ,
866+ ) ;
867+ } ) ;
868+
869+ it ( 'DELETE without body works via buffered delegation' , async ( ) => {
870+ const outerApp = createBodySafeVercelApp ( ) ;
871+
872+ const res = await outerApp . request ( '/api/v1/data/account' , {
873+ method : 'DELETE' ,
874+ } ) ;
875+ expect ( res . status ) . toBe ( 200 ) ;
876+ expect ( mockDispatcher . dispatch ) . toHaveBeenCalledWith (
877+ 'DELETE' ,
878+ '/data/account' ,
879+ undefined ,
880+ expect . any ( Object ) ,
881+ expect . objectContaining ( { request : expect . anything ( ) } ) ,
882+ '/api/v1' ,
883+ ) ;
884+ } ) ;
885+
886+ it ( 'POST with empty body defaults to {} via buffered delegation' , async ( ) => {
887+ const outerApp = createBodySafeVercelApp ( ) ;
888+
889+ const res = await outerApp . request ( '/api/v1/data/account' , {
890+ method : 'POST' ,
891+ headers : { 'Content-Type' : 'application/json' } ,
892+ body : '' ,
893+ } ) ;
894+ expect ( res . status ) . toBe ( 200 ) ;
895+ // Empty body falls back to {} via .catch(() => ({})) in the adapter
896+ expect ( mockDispatcher . dispatch ) . toHaveBeenCalledWith (
897+ 'POST' ,
898+ '/data/account' ,
899+ { } ,
900+ expect . any ( Object ) ,
901+ expect . objectContaining ( { request : expect . anything ( ) } ) ,
902+ '/api/v1' ,
903+ ) ;
904+ } ) ;
905+ } ) ;
906+
683907 describe ( 'Vercel deployment endpoint smoke tests' , ( ) => {
684908 /**
685909 * These tests validate that the two key deployment-health endpoints
0 commit comments