@@ -2,6 +2,7 @@ import { createServer, type Server, IncomingMessage, ServerResponse } from 'node
22import { AddressInfo , createServer as netCreateServer } from 'node:net' ;
33import { randomUUID } from 'node:crypto' ;
44import { EventStore , StreamableHTTPServerTransport , EventId , StreamId } from '../../src/server/streamableHttp.js' ;
5+ import { WebStandardStreamableHTTPServerTransport } from '../../src/server/webStandardStreamableHttp.js' ;
56import { McpServer } from '../../src/server/mcp.js' ;
67import { CallToolResult , JSONRPCMessage } from '../../src/types.js' ;
78import { AuthInfo } from '../../src/server/auth/types.js' ;
@@ -3112,3 +3113,162 @@ async function createTestServerWithDnsProtection(config: {
31123113 baseUrl : serverUrl
31133114 } ;
31143115}
3116+
3117+ describe ( 'WebStandardStreamableHTTPServerTransport - onerror callback' , ( ) => {
3118+ let transport : WebStandardStreamableHTTPServerTransport ;
3119+ let mcpServer : McpServer ;
3120+ let onerrorSpy : ReturnType < typeof vi . fn < ( error : Error ) => void > > ;
3121+
3122+ /** Shorthand to build a Web Standard Request for direct transport testing. */
3123+ function req ( method : string , opts ?: { body ?: unknown ; headers ?: Record < string , string > } ) : Request {
3124+ const headers : Record < string , string > = { ...opts ?. headers } ;
3125+ if ( method === 'POST' ) {
3126+ headers [ 'Accept' ] ??= 'application/json, text/event-stream' ;
3127+ headers [ 'Content-Type' ] ??= 'application/json' ;
3128+ } else if ( method === 'GET' ) {
3129+ headers [ 'Accept' ] ??= 'text/event-stream' ;
3130+ }
3131+ return new Request ( 'http://localhost/mcp' , {
3132+ method,
3133+ headers,
3134+ body : opts ?. body !== undefined ? ( typeof opts . body === 'string' ? opts . body : JSON . stringify ( opts . body ) ) : undefined
3135+ } ) ;
3136+ }
3137+
3138+ function withSession ( sessionId : string , extra ?: Record < string , string > ) : Record < string , string > {
3139+ return { 'mcp-session-id' : sessionId , 'mcp-protocol-version' : '2025-11-25' , ...extra } ;
3140+ }
3141+
3142+ beforeEach ( async ( ) => {
3143+ onerrorSpy = vi . fn < ( error : Error ) => void > ( ) ;
3144+ mcpServer = new McpServer ( { name : 'test-server' , version : '1.0.0' } ) ;
3145+ transport = new WebStandardStreamableHTTPServerTransport ( { sessionIdGenerator : ( ) => randomUUID ( ) } ) ;
3146+ transport . onerror = onerrorSpy ;
3147+ await mcpServer . connect ( transport ) ;
3148+ } ) ;
3149+
3150+ afterEach ( async ( ) => {
3151+ await transport . close ( ) ;
3152+ } ) ;
3153+
3154+ async function initializeServer ( ) : Promise < string > {
3155+ onerrorSpy . mockClear ( ) ;
3156+ const response = await transport . handleRequest ( req ( 'POST' , { body : TEST_MESSAGES . initialize } ) ) ;
3157+ expect ( response . status ) . toBe ( 200 ) ;
3158+ return response . headers . get ( 'mcp-session-id' ) as string ;
3159+ }
3160+
3161+ it ( 'should call onerror for invalid JSON in POST' , async ( ) => {
3162+ await initializeServer ( ) ;
3163+ await transport . handleRequest ( req ( 'POST' , { body : 'not valid json' } ) ) ;
3164+ expect ( onerrorSpy ) . toHaveBeenCalled ( ) ;
3165+ expect ( onerrorSpy . mock . calls [ 0 ] ! [ 0 ] ! . message ) . toMatch ( / I n v a l i d J S O N / ) ;
3166+ } ) ;
3167+
3168+ it ( 'should call onerror for invalid JSON-RPC message' , async ( ) => {
3169+ const sid = await initializeServer ( ) ;
3170+ await transport . handleRequest ( req ( 'POST' , { body : { not : 'valid' } , headers : withSession ( sid ) } ) ) ;
3171+ expect ( onerrorSpy ) . toHaveBeenCalled ( ) ;
3172+ expect ( onerrorSpy . mock . calls [ 0 ] ! [ 0 ] ! . message ) . toMatch ( / I n v a l i d J S O N - R P C m e s s a g e / ) ;
3173+ } ) ;
3174+
3175+ it ( 'should call onerror for missing Accept header on POST' , async ( ) => {
3176+ await transport . handleRequest (
3177+ req ( 'POST' , { body : TEST_MESSAGES . initialize , headers : { Accept : 'application/json' , 'Content-Type' : 'application/json' } } )
3178+ ) ;
3179+ expect ( onerrorSpy ) . toHaveBeenCalled ( ) ;
3180+ expect ( onerrorSpy . mock . calls [ 0 ] ! [ 0 ] ! . message ) . toMatch ( / N o t A c c e p t a b l e / ) ;
3181+ } ) ;
3182+
3183+ it ( 'should call onerror for unsupported Content-Type' , async ( ) => {
3184+ await transport . handleRequest (
3185+ req ( 'POST' , {
3186+ body : TEST_MESSAGES . initialize ,
3187+ headers : { Accept : 'application/json, text/event-stream' , 'Content-Type' : 'text/plain' }
3188+ } )
3189+ ) ;
3190+ expect ( onerrorSpy ) . toHaveBeenCalled ( ) ;
3191+ expect ( onerrorSpy . mock . calls [ 0 ] ! [ 0 ] ! . message ) . toMatch ( / U n s u p p o r t e d M e d i a T y p e / ) ;
3192+ } ) ;
3193+
3194+ it ( 'should call onerror when server is not initialized' , async ( ) => {
3195+ await transport . handleRequest ( req ( 'POST' , { body : TEST_MESSAGES . toolsList } ) ) ;
3196+ expect ( onerrorSpy ) . toHaveBeenCalledTimes ( 1 ) ;
3197+ expect ( onerrorSpy . mock . calls [ 0 ] ! [ 0 ] ! . message ) . toMatch ( / S e r v e r n o t i n i t i a l i z e d / ) ;
3198+ } ) ;
3199+
3200+ it ( 'should call onerror for invalid session ID' , async ( ) => {
3201+ await initializeServer ( ) ;
3202+ await transport . handleRequest ( req ( 'POST' , { body : TEST_MESSAGES . toolsList , headers : withSession ( 'invalid-session-id' ) } ) ) ;
3203+ expect ( onerrorSpy ) . toHaveBeenCalled ( ) ;
3204+ expect ( onerrorSpy . mock . calls [ 0 ] ! [ 0 ] ! . message ) . toMatch ( / S e s s i o n n o t f o u n d / ) ;
3205+ } ) ;
3206+
3207+ it ( 'should call onerror for re-initialization attempt' , async ( ) => {
3208+ await initializeServer ( ) ;
3209+ await transport . handleRequest ( req ( 'POST' , { body : TEST_MESSAGES . initialize } ) ) ;
3210+ expect ( onerrorSpy ) . toHaveBeenCalled ( ) ;
3211+ expect ( onerrorSpy . mock . calls [ 0 ] ! [ 0 ] ! . message ) . toMatch ( / S e r v e r a l r e a d y i n i t i a l i z e d / ) ;
3212+ } ) ;
3213+
3214+ it ( 'should call onerror for missing Accept header on GET' , async ( ) => {
3215+ const sid = await initializeServer ( ) ;
3216+ await transport . handleRequest ( req ( 'GET' , { headers : { Accept : 'application/json' , ...withSession ( sid ) } } ) ) ;
3217+ expect ( onerrorSpy ) . toHaveBeenCalled ( ) ;
3218+ expect ( onerrorSpy . mock . calls [ 0 ] ! [ 0 ] ! . message ) . toMatch ( / N o t A c c e p t a b l e / ) ;
3219+ } ) ;
3220+
3221+ it ( 'should call onerror for concurrent SSE streams' , async ( ) => {
3222+ const sid = await initializeServer ( ) ;
3223+ const response1 = await transport . handleRequest ( req ( 'GET' , { headers : withSession ( sid ) } ) ) ;
3224+ expect ( response1 . status ) . toBe ( 200 ) ;
3225+ await transport . handleRequest ( req ( 'GET' , { headers : withSession ( sid ) } ) ) ;
3226+ expect ( onerrorSpy ) . toHaveBeenCalled ( ) ;
3227+ expect ( onerrorSpy . mock . calls [ 0 ] ! [ 0 ] ! . message ) . toMatch ( / O n l y o n e S S E s t r e a m / ) ;
3228+ } ) ;
3229+
3230+ it ( 'should call onerror for unsupported protocol version' , async ( ) => {
3231+ const sid = await initializeServer ( ) ;
3232+ await transport . handleRequest (
3233+ req ( 'POST' , { body : TEST_MESSAGES . toolsList , headers : withSession ( sid , { 'mcp-protocol-version' : 'unsupported-version' } ) } )
3234+ ) ;
3235+ expect ( onerrorSpy ) . toHaveBeenCalled ( ) ;
3236+ expect ( onerrorSpy . mock . calls [ 0 ] ! [ 0 ] ! . message ) . toMatch ( / U n s u p p o r t e d p r o t o c o l v e r s i o n / ) ;
3237+ } ) ;
3238+
3239+ it ( 'should call onerror for unsupported HTTP methods' , async ( ) => {
3240+ await transport . handleRequest ( req ( 'PUT' ) ) ;
3241+ expect ( onerrorSpy ) . toHaveBeenCalledTimes ( 1 ) ;
3242+ expect ( onerrorSpy . mock . calls [ 0 ] ! [ 0 ] ! . message ) . toMatch ( / M e t h o d n o t a l l o w e d / ) ;
3243+ } ) ;
3244+
3245+ it ( 'should call onerror for invalid event ID in replay' , async ( ) => {
3246+ const eventStore : EventStore = {
3247+ async storeEvent ( ) : Promise < EventId > {
3248+ return 'evt-1' ;
3249+ } ,
3250+ async getStreamIdForEventId ( ) : Promise < StreamId | undefined > {
3251+ return undefined ;
3252+ } ,
3253+ async replayEventsAfter ( ) : Promise < StreamId > {
3254+ return 'stream-1' ;
3255+ }
3256+ } ;
3257+ const storeTransport = new WebStandardStreamableHTTPServerTransport ( { sessionIdGenerator : ( ) => randomUUID ( ) , eventStore } ) ;
3258+ const storeSpy = vi . fn < ( error : Error ) => void > ( ) ;
3259+ storeTransport . onerror = storeSpy ;
3260+ await new McpServer ( { name : 'test' , version : '1.0.0' } ) . connect ( storeTransport ) ;
3261+
3262+ const initResp = await storeTransport . handleRequest ( req ( 'POST' , { body : TEST_MESSAGES . initialize } ) ) ;
3263+ const sid = initResp . headers . get ( 'mcp-session-id' ) as string ;
3264+ storeSpy . mockClear ( ) ;
3265+
3266+ const response = await storeTransport . handleRequest (
3267+ req ( 'GET' , { headers : { ...withSession ( sid ) , 'Last-Event-ID' : 'unknown-event-id' } } )
3268+ ) ;
3269+ expect ( response . status ) . toBe ( 400 ) ;
3270+ expect ( storeSpy ) . toHaveBeenCalledTimes ( 1 ) ;
3271+ expect ( storeSpy . mock . calls [ 0 ] ! [ 0 ] ! . message ) . toMatch ( / I n v a l i d e v e n t I D f o r m a t / ) ;
3272+ await storeTransport . close ( ) ;
3273+ } ) ;
3274+ } ) ;
0 commit comments