На nodejs сервере с соединением HTTP / 2 koa-body зависает из-за отсутствия события END от readStream - PullRequest
0 голосов
/ 19 января 2020

У меня проблемы с запросами POST через соединение HTTP / 2.

Сервер находится на узле 13.6.0 с использованием http2.createSecureServer.

У меня есть Коа. js 2.8.1, с коа-телом 4.1.1, работающим с телом на согласованных маршрутах (через тело и необработанное тело). Клиент использует выборку ES6 для POST json содержимого.

Ошибка возникает, когда raw-body вызывает readStream (). Raw-body захватывает все данные тела, но затем он никогда не получает событие 'END' из потока, поэтому он зависает на неопределенное время.

НО - это происходит только в очень специфических c условиях: a.) Это никогда не происходит при первом POST (и никогда при GET); б.) Это не происходит с дополнительными запросами POST, если они следуют друг за другом менее чем за 10 секунд - это будет работать весь день; c.) Но он потерпит неудачу на 100%, когда POST-запрос будет следовать за предыдущим POST на 10 СЕКУНД ИЛИ БОЛЬШЕ. Кажется, что где-то срабатывает таймер.

Вот и все. У меня есть обходной путь: я изменил raw-body, чтобы метод onData следил за количеством байтов - как только он получает ожидаемое количество байтов, он вызывает onEnd () и выдает ошибку. Это работает отлично, но не в долгосрочной перспективе.

Я предполагаю, что у меня где-то болтается обещание или какая-то другая ошибка, но какое-то оставшееся состояние, кажется, портит мои потоки HTTP / 2. Соединение остается открытым, а сервер обрабатывает другие потоки. Если вы добавите другой POST-запрос, зависший поток выдаст свое событие END, и raw-body продолжит работу, но затем новый будет зависать в той же точке, поэтому клиент фактически получает содержимое из ранее зависшего POST, но новый запрос остается зависшим. и т. д.

Промежуточное программное обеспечение выглядит следующим образом:

  app.use( async (ctx, next) => {
    try {
      //const { socket: { alpnProtocol } } = ctx.req.httpVersion === '2.0' ? ctx.req.stream.session : ctx.req;      
      console.log('app: got request httpVersion='+ctx.req.httpVersion +' :method='+ ctx.req.headers[':method'] + ' :path='+ JSON.stringify(ctx.req.headers[':path']) ); //+' alpnProtocol='+alpnProtocol );
      await next()
    } catch (err) {
      console.log('caught error err='+err);
      ctx.body = { message: err.message }
      ctx.status = err.status || 500
    }
  })

    app.use( helmet() );  // implement security headers, here using helmet default values to trap out cross-browser, etc.
    app.use( favicon(__dirname + '/public/favicon.ico'));  // serve the favicon; __dirname is node.js global for dir of the currently executing script.
    app.use( logger() );  // make Bunyan logger available
    app.use( koa_static( path.join(__dirname, 'public')) );  // __dirname is a node.js global variable designating the working dir of the currently executing script.
    app.use( methodOverride() );  // reset node's 'req.method', e.g. submit via POST, but add header "X-HTTP-Method-Override: PUT" (or DELETE);
    app.use( session( { store: koa_redis({ host: redis_host }) }, app)  );
    app.use( passport.initialize());  // this binds the passport instance to the session introduced in the middleware stack above (initialize is in node_modules/passport/lib/middleware/initialize.js)
    app.use( passport.authenticate( 'session') ); // first try to authenticate via session id; this will pass on sessions but fail on api 'bearer token' requests
    app.use( forceSSL( {port: app.config_ssl.port_force_ssl } ) ); // redirect all calls on port 80 to port 443.  
    app.use( public_router.routes() );
    /*
    AFTER the public router runs, authenticate via 'bearer' in node_modules_mod;
    In this modified version, node_modules_mod exits immediately if a valid session ID is found; otherwise tries authenticating a bearer token
    This must be applied AFTER the public router checks for a match, to expose the home and login page without any authentication
    */
    app.use( passport.authenticate('bearer', { session:false, failureRedirect:'/login/' } ) );  
    app.use( async ( ctx, next ) => {
      if ( ctx.isAuthenticated() ){ 
        let t1 = new Date().getTime();  // ms in epoch
        console.log('\napp.js: user authenticated at '+t1+ ' '+moment().format('LLLL')  );  
        await next()
      } else {
        console.log('\napp.js: router finds user not authenticated for: '+JSON.stringify(this.request));
        res.statusCode = status || 302;
        res.setHeader('Location', '/login/');
        res.setHeader('Content-Length', '0');
        res.end();
      }
    })
    app.use( secure_router.routes() );

    var ssl_options = {
       key: fs.readFileSync( app.config_ssl['ssl_options_key']),  // './ssl/gcrt443-key.pem'
       cert: fs.readFileSync( app.config_ssl['ssl_options_cert']),  // './ssl/gcrt443-cert.pem'
       allowHTTP1: true
    }
    http.createServer( app.callback() ).listen( app.config_ssl.port );
    http2.createSecureServer( ssl_options, app.callback() ).listen( app.config_ssl.port_ssl );

Клиент использует JavaScript ES6 fetch: submit_pdata: (p, data) => {

    return fetch( p, {
        method: 'POST', // or 'PUT'
        //mode: 'cors', // no-cors, *cors, same-origin
        cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
        credentials: 'same-origin', // include, *same-origin, omit
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        },
        body: JSON.stringify( data ) // data can be `string` or {object}!
    })
    .then( res => {
        return res.json() 
    })
    .then( res => { 
        if( res && res.success ){  
             ...process...
        } else { console.log('Error: server reports: '+ ( res? JSON.stringify(res.errfor) : ' null response' ) ); }
    })
    .catch( e => console.error('Error:', e) );
...