hapijs / hapi

The Simple, Secure Framework Developers Trust
https://hapi.dev
Other
14.63k stars 1.34k forks source link

Server extension hook is being called twice #3023

Closed Dindaleon closed 8 years ago

Dindaleon commented 8 years ago

Hello, I wonder what might be causing my onPreResponse hook to be called twice when rendering?

This is my server file:

/* eslint-disable no-console */

// Webpack imports
import webpack from 'webpack';
import WebpackPlugin from 'hapi-webpack-plugin';
import webpackConfig from '../webpack/dev.config';

// Hapi server imports
import Hapi from 'hapi';
import Inert from 'inert';
import Vision from 'vision';
import swagger from 'hapi-swagger';
import jwt from 'hapi-auth-jwt2';
import api from '../src/api';
import rooms from '../src/rooms';
import issueToken from '../src/issueToken';

// React imports
import React from 'react';
import { renderToString } from 'react-dom/server';

// React-router routes and history imports
import getRoutes from '../src/routes';
import createHistory from 'history/lib/createMemoryHistory';

// Redux imports
import { Provider } from 'react-redux';

// Import Intl
import { IntlProvider } from 'react-intl';
import * as lang from '../src/lang';

// Configure Redux Store
import configureStore from '../src/store/configureStore';
import { setUserAgent } from '../src/actions/userActions';
import { setActiveReducers } from '../src/actions/extensionsActions';

// Redux router imports
import { ReduxRouter } from 'redux-router';
import { reduxReactRouter, match } from 'redux-router/server';
import qs from 'query-string';

// Import helpers
import ApiClient from '../src/helpers/ApiClient';
import Html from '../src/helpers/Html';
import { getReducers }from '../src/helpers/activeReducers';

// Import config file
import config from '../src/config';

// Start server function
export default function( callback ) {
  // Create the Walmart Labs Hapi Server
  const server = new Hapi.Server({ debug: { request: [ 'error', 'request-internal' ] }});
  // Configure connections
  server.connection({ host: SERVER_HOST, port: SERVER_PORT, labels: [ 'api' ], routes: { cors: true }});
  server.connection({ host: SERVER_HOST, port: WS_PORT, labels: [ 'ws' ], routes: { cors: true }});

  server.connections[0].name = 'API';
  server.connections[1].name = 'WS';

  const apiServer = server.select('api');
  const wsServer = server.select('ws');

  // Webpack compiler
  const compiler = webpack( webpackConfig );
  const assets = {
    // webpack-dev-middleware options
    // See https://github.com/webpack/webpack-dev-middleware
    publicPath: '/',
    contentBase: 'src',
    stats: {
      colors: true,
      hash: false,
      timings: true,
      chunks: false,
      chunkModules: false,
      modules: false
    }
  };
  const hot = {
    // webpack-hot-middleware options
    // See https://github.com/glenjamin/webpack-hot-middleware
    timeout: '20000',
    reload: true
  };

  server.register([
    {
      register: Inert
    }, {
      register: Vision
    }, {
      register: WebpackPlugin,
      options: { compiler, assets, hot }
    }, {
      register: jwt
    }, {
      register: issueToken
    }, {
      register: api,
      routes: {
        prefix: config.api.routes.path
      }
    }, {
      register: rooms,
      options: {
        server: wsServer
      }
    }
  ], (err) => {
    if (err) {
      throw err;
    }
    server.register([
      {
        register: swagger,
        options: {
          documentationPath: config.api.swagger.documentationPath
        }
      }
    ], { select: [ 'api' ] }, ( error ) => {
      if ( error ) {
        return console.error( error );
      }
      /**
       * Attempt to serve static requests from the public folder.
       */
      apiServer.route({
        method: 'GET',
        path: '/{param*}',
        handler: {
          directory: {
            path: 'static'
          }
        }
      });

      /**
       * Cookie Settings
       */
      apiServer.state('USER_SESSION', {
        ttl: null,
        isSecure: !__DEVELOPMENT__,
        isHttpOnly: false,
        encoding: 'none',
        clearInvalid: false, // remove invalid cookies
        strictHeader: false // don't allow violations of RFC 6265
      });

      apiServer.ext( 'onPreResponse', ( request, reply ) => {
        getReducers().then( activeReducers => {
          if ( typeof request.response.statusCode !== 'undefined' ) {
            return reply.continue();
          }
          const client = new ApiClient(request);
          const store = configureStore(reduxReactRouter, getRoutes, activeReducers, createHistory, client);

          const output = (
            renderToString( <Html store={ store }/> )
          );

          const hydrateOnClient = () => {
            reply( '<!doctype html>\n' + output ).code(500);
          };

          return store.dispatch( match( request.path, ( error, redirectLocation, routerState ) => {
            if ( redirectLocation ) {
              reply.redirect( redirectLocation.pathname + redirectLocation.search );
            } else if ( error || !routerState ) {
              hydrateOnClient();
            } else if ( routerState ) {
              if (routerState.location.search && !routerState.location.query) {
                routerState.location.query = qs.parse(routerState.location.search);
              }
              const promises = [];
              promises.push(store.dispatch(setActiveReducers(activeReducers)));
              Promise.all(promises).then(() => {
                store.dispatch(
                  setUserAgent(request.headers['user-agent']),
                  store.getState().router.then(() => {
                    const component = (
                      <Provider store={ store } key="provider">
                        <IntlProvider
                          key={ store.getState().user.data.locale }
                          locale={ store.getState().user.data.locale }
                          messages={ lang[store.getState().user.data.locale] }>
                          <ReduxRouter />
                        </IntlProvider>
                      </Provider>
                    );

                    const output = (
                        renderToString( <Html component={ component } store={ store } /> )
                      );

                    reply( '<!doctype html>\n' + output);
                  })
                  .catch(err => reply('error: ' + err))
                );
              });
            }
          }));
        });
      });
    });
  });
  // Start Development Server
  return server.start(() => callback( server ));
}
hueniverse commented 8 years ago

Do you know it's the same request and not two requests? Can you log the request id?

Dindaleon commented 8 years ago

I dont know if this helps or not, this is the output of request.getLog

first request:

[ { request: '1453154298763:My-PC:10504:ijkijpii:10000',
    timestamp: 1453154298766,
    tags: [ 'received' ],
    data:
     { method: 'get',
       url: '/home',
       agent: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36' },
    internal: true },
  { request: '1453154298763:My-PC:10504:ijkijpii:10000',
    timestamp: 1453154300378,
    tags: [ 'handler', 'error' ],
    data: { msec: 203.54270800948143, error: 'Not Found', data: [Object] },
    internal: true } ]

second request:

[ { request: '1453154300491:My-PC:10504:ijkijpii:10001',
    timestamp: 1453154300492,
    tags: [ 'received' ],
    data:
     { method: 'get',
       url: '/api/v1/oauth/token',
       agent: 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)' },
    internal: true },
  { request: '1453154300491:My-PC:10504:ijkijpii:10001',
    timestamp: 1453154300497,
    tags: [ 'handler' ],
    data: { msec: 1.190521001815796 },
    internal: true } ]

Oh! now I see. I am making two requests on load, one for rendering the page, in this case Home, and the other request is for checking a user's credentials.

Is this acceptable or should I look for a way to improve it? Maybe having api calls on another connection?

hueniverse commented 8 years ago

I don't understand your problem.

Dindaleon commented 8 years ago

My problem is that server rendering is happening twice. After you told me to check the number of requests and their ids, I could verify that there were two requests being made indeed. So it is not an issue of hapi. Then, I just asked if it was ok to have two or more requests on the same connection or if it is better to split them into two connections were the main connection is used for rendering the website and the other connection for api calls.

hueniverse commented 8 years ago

Are you trying to render only some routes and serve json on others? You can set ext on specific routes or check a flag in the ext for some endpoints. There are many patterns.This would be better raised in hapijs/discuss.

Dindaleon commented 8 years ago

Yes, there is another route that serves json only. I will raise my issue on hapijs/discuss, no problem. Thank you.