This article is participating in node.js advanced technology essay, click to see more details

Finalhandler is the processing logic in NodeJs as the last step in the HTTP service response. Said some popular, is the automatic response when an error 404 | 500, etc. This article will be based on 1.1.2 version of the handler library to explain its basic use and principle implementation, there is no lack of dry goods oh!!

Basic use of Http 404 and 500 responses

  • For allhttpAll requests respond404error
const http = require('http');
const finalhandler = require('finalhandler');

const server = http.createServer((req, res) = > {
  const done = finalhandler(req, res);
  done();
});

server.listen(3000.() = > {
  console.log('[server] running at port 3000.');
});
Copy the code

The result is an automatic response to a 404 error as shown below:

  • righthttpRequest returnsstreamFlow, and respond when an error occurs500error
const http = require('http');
const fs = require('fs');
const finalhandler = require('.. /index');

// Prints error logs when errors occur
function logger(err) {
  console.error(err.stack || err.toString());
}

// Initialize the HTTP service
const server = http.createServer((req, res) = > {
  const done = finalhandler(req, res, {
    onerror: logger,
  });

  // Create a readable stream and read a file that does not exist
  const stream = fs.createReadStream('not/exist/path/demo.txt');
  stream.on('error'.(err) = > {
    // Response 500 error
    done(err);
  });
  stream.pipe(res);
});

server.listen(3000.() = > {
  console.log('[server] running at port 3000.');
});
Copy the code

The result is 500 error response as shown below:

The source code parsing

The main source is to export a finalHandler function that handles the initialization of parameters and returns a function that accepts an err object passed in from the outside.

/**
 * Module exports.
 * @public* /

module.exports = finalhandler

/** * create a function to process the last logical step of response **@param {Request} req
 * @param {Response} res
 * @param {Object} [options]
 * @return {Function}
 * @public* /

function finalhandler (req, res, options) {
    // Parameter processing
    / / to omit...
    
    // Return a function
    return function (err) {}}Copy the code

Take a look at the complete finalHandler logic. The argument handling part is mainly to get the env environment variable, the callback function when an error occurs.

function finalhandler (req, res, options) {
  var opts = options || {}

  // Get environment variables
  var env = opts.env || process.env.NODE_ENV || 'development'

  // Get the callback function when an error occurs
  var onerror = opts.onerror

  // Return a function, also a closure
  return function (err) {
    var headers
    var msg
    var status

    // ignore 404 on in-flight response
    // The header has already been sent, so 404 cannot be responded to
    // writeHead has been done before
    if(! err && headersSent(res)) { debug('cannot 404 after headers sent')
      return
    }

    // unhandled error
    if (err) {
      // Get the error status code from the error object
      status = getErrorStatusCode(err)

      if (status === undefined) {
        // Fall back to the status code on the Response object
        status = getResponseStatusCode(res)
      } else {
        // Get headers from the error object
        headers = getErrorHeaders(err)
      }

      // Generate an error message
      msg = getErrorMessage(err, status, env)
    } else {
      // Define 404 status codes and error messages
      status = 404
      msg = 'Cannot ' + req.method + ' ' + encodeUrl(getResourceName(req))
    }

    debug('default %s', status)

    // If err exists and the user defines the callback of ERR, the callback is triggered
    if (err && onerror) {
      defer(onerror, err, req, res)
    }

    // The reQ stream is destroyed if it cannot respond
    if (headersSent(res)) {
      debug('cannot %d after headers sent', status)
      req.socket.destroy()
      return
    }

    // Call the send method to handle the response sending logic
    send(req, res, status, headers, msg)
  }
}
Copy the code

The function returned is also a closure, and its main logic is:

  • throughheadersSent(res)Determine if theheaderHas been sent, thenlogOne message, no processing
  • There areerrObject, such as when we callif (err) done(err)The approach was passed inerrobject
    • Try to get the error status code
      • First fetch from the error object, and then try to fetch from iterrFetch on objectheaders. thisheadersIf it does, it will be carried in the library’s final response.
      • errObject cannot be retrieved fromresponseFetch on object
      • The default error status code is500
    • Try to get an error message
  • errObject does not exist, as we calldone()
    • The error status code is set to404
    • Error messages are directly set to for exampleCannot Get /path/your/req
  • Even if theerrObject exists, butheaderIt’s been sent out. It’s being destroyedreqStream, no more processing
  • If the user has set error handlingcallbackThe calldefer(onerror, err, req, res)The function handles callbacks to callback functions
  • Otherwise the last callsendThe function sends the response logic

Take a look at the tool method implementations above:

  • headersSentThe header function checks whether the header has already been sent
/** * Check whether the header has been sent *@param {object} res
 * @returns {boolean}
 * @private* /
function headersSent (res) {
  return typeofres.headersSent ! = ='boolean'
    ? Boolean(res._header)
    : res.headersSent
}
Copy the code

The value of this field will be set to true after the header is sent. Here is an example:

const http = require('http');

const server = http.createServer((req, res) = > {
  // false
  console.log('berfor headersSent', res.headersSent);
  res.writeHead(200);
  // true
  console.log('after headersSent', res.headersSent);
  res.end();
});

server.listen(3200);
Copy the code
  • Function implementation to get the error status code
/** * Get the Error status code ** from the Error object@param {Error} err
 * @return {number}
 * @private* /

function getErrorStatusCode (err) {
  // check err.status
  if (typeof err.status === 'number' && err.status >= 400 && err.status < 600) {
    return err.status
  }

  // check err.statusCode
  if (typeof err.statusCode === 'number'
    && err.statusCode >= 400
    && err.statusCode < 600
  ) {
    return err.statusCode
  }

  return undefined
}

/** * get the status code ** from the response object@param {OutgoingMessage} res
 * @return {number}
 * @private* /

function getResponseStatusCode (res) {
  var status = res.statusCode

  // If the status code does not exist on the response, or the value of the status code is not in the range of 400-599,
  // The default status code is 500
  // default status code to 500 if outside valid range
  if (typeofstatus ! = ='number' || status < 400 || status > 599) {
    status = 500
  }

  return status
}
Copy the code
  • The logical implementation of getting headers from an ERR object

The logic here is to check whether the Err object has the headers field. If so, make a copy of the object and return it.

/** * get headers from the Error object@param {Error} err
 * @return {object}
 * @private* /

function getErrorHeaders (err) {
  // Undefined is returned if headers is not present on err or the format is incorrect
  if(! err.headers ||typeoferr.headers ! = ='object') {
    return undefined
  }

  // Copy all keys/values of headers
  var headers = Object.create(null)
  var keys = Object.keys(err.headers)

  for (var i = 0; i < keys.length; i++) {
    var key = keys[i]
    headers[key] = err.headers[key]
  }

  return headers
}
Copy the code
  • Get the implementation of the error message

In order to obtain the error information, the first step is to determine whether it is the production environment. For security reasons, the production environment cannot expose the specific error stack information, which is not safe. This is very important!!

Therefore, this function also tries to get the error stack information only in non-production environments. If not, it tries to get the error information through err.tostring (). If not, it gets the general error information through the Statuses library. For example, 500 returns Internal Server Error, 501 returns Not Implemented, 502 returns Bad Gateway, and so on.

The statuses library is also the only way to get generic error messages in production.

/** * Get the Error message from the Error object, if not, get the general Error message from status *@param {Error} err
 * @param {number} status
 * @param {string} env
 * @return {string}
 * @private* /
function getErrorMessage (err, status, env) {
  var msg

  // In non-production environments, try to get specific MSG information
  // The build environment does not expose specific error stacks and other information because it is not secure
  if(env ! = ='production') {
    // Stack information is preferred because stack information contains message information
    msg = err.stack

    // If there is no stack information, try toString
    if(! msg &&typeof err.toString === 'function') {
      msg = err.toString()
    }
  }

  The statuses library provides generic error messages for production and development environments
  // for example 500 -> "Internal Server Error"
  return msg || statuses[status]
}
Copy the code
  • The realization of the defer
/** * Wait until the current run ends ** -process.nexttick () belongs to idle observers * -setimmediate () does not belong to the idle observer * -Setimmediate () does not belong to the check observer * - The idle observer precedes the I/O observer */
var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function (fn) { process.nextTick(fn.bind.apply(fn, arguments))}var isFinished = onFinished.isFinished
Copy the code

The implementation of defer should be noted that process.nextTick puts the asynchronous callback at the end of the current frame and before the IO callback. If there is too much nextTick, the IO callback will continue to be delayed and finally the callback will pile up too much. SetImmediate, however, does not affect the IO callbacks and does not cause callbacks to pile up. So Defer uses the setImmediate method in preference.

Let’s take a look at the logic of the send function that actually sends the response

/** * Send response response **@param {IncomingMessage} The req request *@param {OutgoingMessage} Res response *@param {number} Status Indicates the response status *@param {object} headers
 * @param {string} message
 * @private* /
function send (req, res, status, headers, message) {
  function write () {
    / / the response HTML
    var body = createHtmlDocument(message)

    // Set the status code of the response and the status code information
    res.statusCode = status
    res.statusMessage = statuses[status]

    // If headers exists on the err object in the previous step
    // This method sets the values of the headers object to the response header
    setHeaders(res, headers)

    // CSP policy to prevent XSS attacks
    res.setHeader('Content-Security-Policy'."default-src 'none'")
    // Tell the receiver not to sniff MIME types, that is, the server validates its MIME Settings
    res.setHeader('X-Content-Type-Options'.'nosniff')

    // Set the Content type and length of the response header
    res.setHeader('Content-Type'.'text/html; charset=utf-8')
    res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))

    // Support the HEAD request
    if (req.method === 'HEAD') {
      res.end()
      return
    }

    // Set the response data
    res.end(body, 'utf8')}// Triggered when the request fails, closes, or completes
  if (isFinished(req)) {
    write()
    return
  }

  // Disconnect all pipe connections on req,
  // call req.unpipe(),
  // unpipe() does not pass an argument to disconnect all pipes, but an argument to disconnect a specified pipe
  unpipe(req)

  // Call write function to send response after request ends
  onFinished(req, write)
  // Restore reQ to flow state
  // Req is paused after calling unpipe
  req.resume()
}
Copy the code

There are two types of send processing logic:

  • reqFinished (e.g. request closed, error, completed)
    • Create the body of the response, the HTML content, to respond to
    • Set the response code and the response code information
    • If beforeerrExists on an objectheadersThe response header related fields on the response object are set in turn
    • Set security-related response headers
    • Set up theConetentAssociated response headers
    • If it isHEADRequests return only the response header
    • Sending response data
    • To deal with end
  • reqRequest not completed
    • callunpipeLibrary ends to disconnectreqAll the pipes on the. Be aware that this willreqSet to pause.
    • Wait for the request to end before invoking the reQ request completion processing logic
    • Finally, reQ is reset to flow

Note here that the unPIPE library is called to terminate all pipe connections on the REQ before the reQ request ends:

  • Behind the unpipe library is the call to req.unpipe()

    • callunpipeMethod disconnects all pipe connections if no argument is passed
    • callunpipeMethod is the pipe connection specified by the port.
  • Req is a readable stream, and req.unpipe() changes the state of the readable stream to pause, temporarily stopping the flow of events. Note that it does not stop the generation of data. So after processing is complete, req.resume() is called to restore the flow from the paused state to the flowing state.

See the Node Stream unpipe method for more information about streams.

Finally, take a look at the returned HTML generation logic:

var DOUBLE_SPACE_REGEXP = /\x20{2}/g
var NEWLINE_REGEXP = /\n/g

/** * Create the HTML for the response **@param {string} message
 * @private* /
function createHtmlDocument (message) {
  // escapeHtml encoding for message for security reasons
  // Then convert the newline character to a 

tag
// Finally deal with the problem of multiple Spaces being displayed correctly var body = escapeHtml(message) .replace(NEWLINE_REGEXP, '<br>') .replace(DOUBLE_SPACE_REGEXP, '   ') return ' \n' + '<html lang="en">\n' + '<head>\n' + '<meta charset="utf-8">\n' + '<title>Error</title>\n' + '</head>\n' + '<body>\n' + '<pre>' + body + '</pre>\n' + '</body>\n' + '</html>\n' } Copy the code

One small note here is that the \r processing is unnecessary because the url argument does not contain line breaks. Another thing to consider is that if you want to handle line breaks, the re should take into account that different systems have different line breaks:

// old
var NEWLINE_REGEXP = /\n/g

// new
var NEWLINE_REGEXP = /\r|\n|\r\n/g
Copy the code

An important point is that since the message message contains the URL requested by the user, which comes from the requestor side, the parameters in the URL are not trusted and may contain XSS injection, so escapeHtml should be used for encoding and reprocessing.

reference

  • Content Security Policy Tutorial Ruan Yifeng
  • X-content-type-options MDN document x-content-type-optionss
  • X-content-type-options MDN document x-content-type-options

conclusion

If you like this article ❤️❤️ 👍👍 👍👍 tweet ❤️❤️❤️ ha ~ ~ ~

I also recommend you to read my other source code parsing class nuggets article:

  • Weekly downloads of 50 million “MIME” library source code to achieve full parsing
  • Take a look at how the Node Attachment download service works in the Content-Disposition source code
  • Explain the implementation principle of NodeJs static file hosting service in “Send” source code
  • Explain the Node middleware model architecture in the source code of Connect
  • NodeJS techniques in live-Server source code
  • Ts Master: 22 examples to delve into Ts’s most obscure advanced type tools 👍 1.5K
  • Here are 28 Canvas libraries that will make you scream “wow!” 👍 852
  • In-depth explanation of VsCode advanced debugging and use skills in various scenarios

Hello everyone, I am Leng Hammer, welcome to follow me ❤️, I and the front end of the story continues……