# @hapi/hapi
#### The Simple, Secure Framework Developers Trust
Build powerful, scalable applications, with minimal overhead and full out-of-the-box functionality - your code, your way.
### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support
## Useful resources
- [Documentation and API](https://hapi.dev/)
- [Version status](https://hapi.dev/resources/status/#hapi) (builds, dependencies, node versions, licenses, eol)
- [Changelog](https://hapi.dev/resources/changelog/)
- [Project policies](https://hapi.dev/policies/)
- [Support](https://hapi.dev/support/)
## Technical Steering Committee (TSC) Members
- Devin Ivy ([@devinivy](https://github.com/devinivy))
- Lloyd Benson ([@lloydbenson](https://github.com/lloydbenson))
- Nathan LaFreniere ([@nlf](https://github.com/nlf))
- Wyatt Lyon Preul ([@geek](https://github.com/geek))
- Nicolas Morel ([@marsup](https://github.com/marsup))
- Jonathan Samines ([@jonathansamines](https://github.com/jonathansamines))
================================================
FILE: SPONSORS.md
================================================
We'd like to thank our sponsors as well as the legacy sponsors who have supported hapi throughout the years. Thanks so much for your support!
> Below are hapi's top recurring sponsors, but there are many more to thank. For the complete list, see [hapi.dev/policies/sponsors](https://hapi.dev/policies/sponsors/) or [hapijs/.github/SPONSORS.md](https://github.com/hapijs/.github/blob/master/SPONSORS.md).
# Staff Sponsors
- [Big Room Studios](https://www.bigroomstudios.com/)
- [Dixeed](https://dixeed.com/)
# Top Sponsors
- Fabian Gündel / [DataWrapper.de](https://www.datawrapper.de/)
- Devin Stewart
- [Raider.IO](https://raider.io/)
- [Florence Healthcare](https://florencehc.com/)
================================================
FILE: lib/auth.js
================================================
'use strict';
const Boom = require('@hapi/boom');
const Bounce = require('@hapi/bounce');
const Hoek = require('@hapi/hoek');
const Config = require('./config');
const Request = require('./request');
const internals = {
missing: Symbol('missing')
};
exports = module.exports = internals.Auth = class {
#core = null;
#schemes = {};
#strategies = {};
api = {}; // Do not reassign api or settings, as they are referenced in public()
settings = {
default: null // Strategy used as default if route has no auth settings
};
constructor(core) {
this.#core = core;
}
public(server) {
return {
api: this.api,
settings: this.settings,
scheme: this.scheme.bind(this),
strategy: this._strategy.bind(this, server),
default: this.default.bind(this),
test: this.test.bind(this),
verify: this.verify.bind(this),
lookup: this.lookup.bind(this)
};
}
scheme(name, scheme) {
Hoek.assert(name, 'Authentication scheme must have a name');
Hoek.assert(!this.#schemes[name], 'Authentication scheme name already exists:', name);
Hoek.assert(typeof scheme === 'function', 'scheme must be a function:', name);
this.#schemes[name] = scheme;
}
_strategy(server, name, scheme, options = {}) {
Hoek.assert(name, 'Authentication strategy must have a name');
Hoek.assert(typeof options === 'object', 'options must be an object');
Hoek.assert(!this.#strategies[name], 'Authentication strategy name already exists');
Hoek.assert(scheme, 'Authentication strategy', name, 'missing scheme');
Hoek.assert(this.#schemes[scheme], 'Authentication strategy', name, 'uses unknown scheme:', scheme);
server = server._clone();
const strategy = this.#schemes[scheme](server, options);
Hoek.assert(strategy.authenticate, 'Invalid scheme:', name, 'missing authenticate() method');
Hoek.assert(typeof strategy.authenticate === 'function', 'Invalid scheme:', name, 'invalid authenticate() method');
Hoek.assert(!strategy.payload || typeof strategy.payload === 'function', 'Invalid scheme:', name, 'invalid payload() method');
Hoek.assert(!strategy.response || typeof strategy.response === 'function', 'Invalid scheme:', name, 'invalid response() method');
strategy.options = strategy.options ?? {};
Hoek.assert(strategy.payload || !strategy.options.payload, 'Cannot require payload validation without a payload method');
this.#strategies[name] = {
methods: strategy,
realm: server.realm
};
if (strategy.api) {
this.api[name] = strategy.api;
}
}
default(options) {
Hoek.assert(!this.settings.default, 'Cannot set default strategy more than once');
options = Config.apply('auth', options, 'default strategy');
this.settings.default = this._setupRoute(Hoek.clone(options)); // Prevent changes to options
const routes = this.#core.router.table();
for (const route of routes) {
route.rebuild();
}
}
async test(name, request) {
Hoek.assert(name, 'Missing authentication strategy name');
const strategy = this.#strategies[name];
Hoek.assert(strategy, 'Unknown authentication strategy:', name);
const bind = strategy.methods;
const realm = strategy.realm;
const response = await request._core.toolkit.execute(strategy.methods.authenticate, request, { bind, realm, auth: true });
if (!response.isAuth) {
throw response;
}
if (response.error) {
throw response.error;
}
return response.data;
}
async verify(request) {
const auth = request.auth;
if (auth.error) {
throw auth.error;
}
if (!auth.isAuthenticated) {
return;
}
const strategy = this.#strategies[auth.strategy];
Hoek.assert(strategy, 'Unknown authentication strategy:', auth.strategy);
if (!strategy.methods.verify) {
return;
}
const bind = strategy.methods;
await strategy.methods.verify.call(bind, auth);
}
static testAccess(request, route) {
const auth = request._core.auth;
try {
return auth._access(request, route);
}
catch (err) {
Bounce.rethrow(err, 'system');
return false;
}
}
_setupRoute(options, path) {
if (!options) {
return options; // Preserve the difference between undefined and false
}
if (typeof options === 'string') {
options = { strategies: [options] };
}
else if (options.strategy) {
options.strategies = [options.strategy];
delete options.strategy;
}
if (path &&
!options.strategies) {
Hoek.assert(this.settings.default, 'Route missing authentication strategy and no default defined:', path);
options = Hoek.applyToDefaults(this.settings.default, options);
}
path = path ?? 'default strategy';
Hoek.assert(options.strategies?.length, 'Missing authentication strategy:', path);
options.mode = options.mode ?? 'required';
if (options.entity !== undefined || // Backwards compatibility with <= 11.x.x
options.scope !== undefined) {
options.access = [{ entity: options.entity, scope: options.scope }];
delete options.entity;
delete options.scope;
}
if (options.access) {
for (const access of options.access) {
access.scope = internals.setupScope(access);
}
}
if (options.payload === true) {
options.payload = 'required';
}
let hasAuthenticatePayload = false;
for (const name of options.strategies) {
const strategy = this.#strategies[name];
Hoek.assert(strategy, 'Unknown authentication strategy', name, 'in', path);
Hoek.assert(strategy.methods.payload || options.payload !== 'required', 'Payload validation can only be required when all strategies support it in', path);
hasAuthenticatePayload = hasAuthenticatePayload || strategy.methods.payload;
Hoek.assert(!strategy.methods.options.payload || options.payload === undefined || options.payload === 'required', 'Cannot set authentication payload to', options.payload, 'when a strategy requires payload validation in', path);
}
Hoek.assert(!options.payload || hasAuthenticatePayload, 'Payload authentication requires at least one strategy with payload support in', path);
return options;
}
lookup(route) {
if (route.settings.auth === false) {
return false;
}
return route.settings.auth || this.settings.default;
}
_enabled(route, type) {
const config = this.lookup(route);
if (!config) {
return false;
}
if (type === 'authenticate') {
return true;
}
if (type === 'access') {
return !!config.access;
}
for (const name of config.strategies) {
const strategy = this.#strategies[name];
if (strategy.methods[type]) {
return true;
}
}
return false;
}
static authenticate(request) {
const auth = request._core.auth;
return auth._authenticate(request);
}
async _authenticate(request) {
const config = this.lookup(request.route);
const errors = [];
request.auth.mode = config.mode;
// Injection bypass
if (request.auth.credentials) {
internals.validate(null, { credentials: request.auth.credentials, artifacts: request.auth.artifacts }, request.auth.strategy, config, request, errors);
return;
}
// Try each strategy
for (const name of config.strategies) {
const strategy = this.#strategies[name];
const bind = strategy.methods;
const realm = strategy.realm;
const response = await request._core.toolkit.execute(strategy.methods.authenticate, request, { bind, realm, auth: true });
const message = (response.isAuth ? internals.validate(response.error, response.data, name, config, request, errors) : internals.validate(response, null, name, config, request, errors));
if (!message) {
return;
}
if (message !== internals.missing) {
return message;
}
}
// No more strategies
const err = Boom.unauthorized('Missing authentication', errors);
if (config.mode === 'required') {
throw err;
}
request.auth.isAuthenticated = false;
request.auth.credentials = null;
request.auth.error = err;
request._log(['auth', 'unauthenticated']);
}
static access(request) {
const auth = request._core.auth;
request.auth.isAuthorized = auth._access(request);
}
_access(request, route) {
const config = this.lookup(route || request.route);
if (!config?.access) {
return true;
}
const credentials = request.auth.credentials;
if (!credentials) {
if (config.mode !== 'required') {
return false;
}
throw Boom.forbidden('Request is unauthenticated');
}
const requestEntity = (credentials.user ? 'user' : 'app');
const scopeErrors = [];
for (const access of config.access) {
// Check entity
const entity = access.entity;
if (entity &&
entity !== 'any' &&
entity !== requestEntity) {
continue;
}
// Check scope
let scope = access.scope;
if (scope) {
if (!credentials.scope) {
scopeErrors.push(scope);
continue;
}
scope = internals.expandScope(request, scope);
if (!internals.validateScope(credentials, scope, 'required') ||
!internals.validateScope(credentials, scope, 'selection') ||
!internals.validateScope(credentials, scope, 'forbidden')) {
scopeErrors.push(scope);
continue;
}
}
return true;
}
// Scope error
if (scopeErrors.length) {
request._log(['auth', 'scope', 'error']);
throw Boom.forbidden('Insufficient scope', { got: credentials.scope, need: scopeErrors });
}
// Entity error
if (requestEntity === 'app') {
request._log(['auth', 'entity', 'user', 'error']);
throw Boom.forbidden('Application credentials cannot be used on a user endpoint');
}
request._log(['auth', 'entity', 'app', 'error']);
throw Boom.forbidden('User credentials cannot be used on an application endpoint');
}
static async payload(request) {
if (!request.auth.isAuthenticated || !request.auth[Request.symbols.authPayload]) {
return;
}
const auth = request._core.auth;
const strategy = auth.#strategies[request.auth.strategy];
Hoek.assert(strategy, 'Unknown authentication strategy:', request.auth.strategy);
if (!strategy.methods.payload) {
return;
}
const config = auth.lookup(request.route);
const setting = config.payload ?? (strategy.methods.options.payload ? 'required' : false);
if (!setting) {
return;
}
const bind = strategy.methods;
const realm = strategy.realm;
const response = await request._core.toolkit.execute(strategy.methods.payload, request, { bind, realm });
if (response.isBoom &&
response.isMissing) {
return setting === 'optional' ? undefined : Boom.unauthorized('Missing payload authentication');
}
return response;
}
static async response(response) {
const request = response.request;
const auth = request._core.auth;
if (!request.auth.isAuthenticated) {
return;
}
const strategy = auth.#strategies[request.auth.strategy];
Hoek.assert(strategy, 'Unknown authentication strategy:', request.auth.strategy);
if (!strategy.methods.response) {
return;
}
const bind = strategy.methods;
const realm = strategy.realm;
const error = await request._core.toolkit.execute(strategy.methods.response, request, { bind, realm, continue: 'undefined' });
if (error) {
throw error;
}
}
};
internals.setupScope = function (access) {
// No scopes
if (!access.scope) {
return false;
}
// Already setup
if (!Array.isArray(access.scope)) {
return access.scope;
}
const scope = {};
for (const value of access.scope) {
const prefix = value[0];
const type = prefix === '+' ? 'required' : (prefix === '!' ? 'forbidden' : 'selection');
const clean = type === 'selection' ? value : value.slice(1);
scope[type] = scope[type] ?? [];
scope[type].push(clean);
if ((!scope._hasParameters?.[type]) &&
/{([^}]+)}/.test(clean)) {
scope._hasParameters = scope._hasParameters ?? {};
scope._hasParameters[type] = true;
}
}
return scope;
};
internals.validate = function (err, result, name, config, request, errors) { // err can be Boom, Error, or a valid response object
result = result ?? {};
request.auth.isAuthenticated = !err;
if (err) {
// Non-error response
if (err instanceof Error === false) {
request._log(['auth', 'unauthenticated', 'response', name], { statusCode: err.statusCode });
return err;
}
// Missing authenticated
if (err.isMissing) {
request._log(['auth', 'unauthenticated', 'missing', name], err);
errors.push(err.output.headers['WWW-Authenticate']);
return internals.missing;
}
}
request.auth.strategy = name;
request.auth.credentials = result.credentials;
request.auth.artifacts = result.artifacts;
// Authenticated
if (!err) {
return;
}
// Unauthenticated
request.auth.error = err;
if (config.mode === 'try') {
request._log(['auth', 'unauthenticated', 'try', name], err);
return;
}
request._log(['auth', 'unauthenticated', 'error', name], err);
throw err;
};
internals.expandScope = function (request, scope) {
if (!scope._hasParameters) {
return scope;
}
const expanded = {
required: internals.expandScopeType(request, scope, 'required'),
selection: internals.expandScopeType(request, scope, 'selection'),
forbidden: internals.expandScopeType(request, scope, 'forbidden')
};
return expanded;
};
internals.expandScopeType = function (request, scope, type) {
if (!scope._hasParameters[type]) {
return scope[type];
}
const expanded = [];
const context = {
params: request.params,
query: request.query,
payload: request.payload,
credentials: request.auth.credentials
};
for (const template of scope[type]) {
expanded.push(Hoek.reachTemplate(context, template));
}
return expanded;
};
internals.validateScope = function (credentials, scope, type) {
if (!scope[type]) {
return true;
}
const count = typeof credentials.scope === 'string' ?
scope[type].indexOf(credentials.scope) !== -1 ? 1 : 0 :
Hoek.intersect(scope[type], credentials.scope).length;
if (type === 'forbidden') {
return count === 0;
}
if (type === 'required') {
return count === scope.required.length;
}
return !!count;
};
================================================
FILE: lib/compression.js
================================================
'use strict';
const Zlib = require('zlib');
const Accept = require('@hapi/accept');
const Bounce = require('@hapi/bounce');
const Hoek = require('@hapi/hoek');
const internals = {
common: ['gzip, deflate', 'deflate, gzip', 'gzip', 'deflate', 'gzip, deflate, br']
};
exports = module.exports = internals.Compression = class {
decoders = {
gzip: (options) => Zlib.createGunzip(options),
deflate: (options) => Zlib.createInflate(options)
};
encodings = ['identity', 'gzip', 'deflate'];
encoders = {
identity: null,
gzip: (options) => Zlib.createGzip(options),
deflate: (options) => Zlib.createDeflate(options)
};
#common = null;
constructor() {
this._updateCommons();
}
_updateCommons() {
this.#common = new Map();
for (const header of internals.common) {
this.#common.set(header, Accept.encoding(header, this.encodings));
}
}
addEncoder(encoding, encoder) {
Hoek.assert(this.encoders[encoding] === undefined, `Cannot override existing encoder for ${encoding}`);
Hoek.assert(typeof encoder === 'function', `Invalid encoder function for ${encoding}`);
this.encoders[encoding] = encoder;
this.encodings.unshift(encoding);
this._updateCommons();
}
addDecoder(encoding, decoder) {
Hoek.assert(this.decoders[encoding] === undefined, `Cannot override existing decoder for ${encoding}`);
Hoek.assert(typeof decoder === 'function', `Invalid decoder function for ${encoding}`);
this.decoders[encoding] = decoder;
}
accept(request) {
const header = request.headers['accept-encoding'];
if (!header) {
return 'identity';
}
const common = this.#common.get(header);
if (common) {
return common;
}
try {
return Accept.encoding(header, this.encodings);
}
catch (err) {
Bounce.rethrow(err, 'system');
err.header = header;
request._log(['accept-encoding', 'error'], err);
return 'identity';
}
}
encoding(response, length) {
if (response.settings.compressed) {
response.headers['content-encoding'] = response.settings.compressed;
return null;
}
const request = response.request;
if (!request._core.settings.compression ||
length !== null && length < request._core.settings.compression.minBytes) {
return null;
}
const mime = request._core.mime.type(response.headers['content-type'] || 'application/octet-stream');
if (!mime.compressible) {
return null;
}
response.vary('accept-encoding');
if (response.headers['content-encoding']) {
return null;
}
return request.info.acceptEncoding === 'identity' ? null : request.info.acceptEncoding;
}
encoder(request, encoding) {
const encoder = this.encoders[encoding];
Hoek.assert(encoder !== undefined, `Unknown encoding ${encoding}`);
return encoder(request.route.settings.compression[encoding]);
}
};
================================================
FILE: lib/config.js
================================================
'use strict';
const Os = require('os');
const Somever = require('@hapi/somever');
const Validate = require('@hapi/validate');
const internals = {};
exports.symbol = Symbol('hapi-response');
exports.apply = function (type, options, ...message) {
const result = internals[type].validate(options);
if (result.error) {
throw new Error(`Invalid ${type} options ${message.length ? '(' + message.join(' ') + ')' : ''} ${result.error.annotate()}`);
}
return result.value;
};
exports.enable = function (options) {
const settings = options ? Object.assign({}, options) : {}; // Shallow cloned
if (settings.security === true) {
settings.security = {};
}
if (settings.cors === true) {
settings.cors = {};
}
return settings;
};
exports.versionMatch = (version, range) => Somever.match(version, range, { includePrerelease: true });
internals.access = Validate.object({
entity: Validate.valid('user', 'app', 'any'),
scope: [false, Validate.array().items(Validate.string()).single().min(1)]
});
internals.auth = Validate.alternatives([
Validate.string(),
internals.access.keys({
mode: Validate.valid('required', 'optional', 'try'),
strategy: Validate.string(),
strategies: Validate.array().items(Validate.string()).min(1),
access: Validate.array().items(internals.access.min(1)).single().min(1),
payload: [
Validate.valid('required', 'optional'),
Validate.boolean()
]
})
.without('strategy', 'strategies')
.without('access', ['scope', 'entity'])
]);
internals.event = Validate.object({
method: Validate.array().items(Validate.function()).single(),
options: Validate.object({
before: Validate.array().items(Validate.string()).single(),
after: Validate.array().items(Validate.string()).single(),
bind: Validate.any(),
sandbox: Validate.valid('server', 'plugin'),
timeout: Validate.number().integer().min(1)
})
.default({})
});
internals.exts = Validate.array()
.items(internals.event.keys({ type: Validate.string().required() })).single();
internals.failAction = Validate.alternatives([
Validate.valid('error', 'log', 'ignore'),
Validate.function()
])
.default('error');
internals.routeBase = Validate.object({
app: Validate.object().allow(null),
auth: internals.auth.allow(false),
bind: Validate.object().allow(null),
cache: Validate.object({
expiresIn: Validate.number(),
expiresAt: Validate.string(),
privacy: Validate.valid('default', 'public', 'private'),
statuses: Validate.array().items(Validate.number().integer().min(200)).min(1).single().default([200, 204]),
otherwise: Validate.string().default('no-cache')
})
.allow(false)
.default(),
compression: Validate.object()
.pattern(/.+/, Validate.object())
.default(),
cors: Validate.object({
origin: Validate.array().min(1).allow('ignore').default(['*']),
maxAge: Validate.number().default(86400),
headers: Validate.array().items(Validate.string()).default(['Accept', 'Authorization', 'Content-Type', 'If-None-Match']),
additionalHeaders: Validate.array().items(Validate.string()).default([]),
exposedHeaders: Validate.array().items(Validate.string()).default(['WWW-Authenticate', 'Server-Authorization']),
additionalExposedHeaders: Validate.array().items(Validate.string()).default([]),
credentials: Validate.boolean().when('origin', { is: 'ignore', then: false }).default(false),
preflightStatusCode: Validate.valid(200, 204).default(200)
})
.allow(false, true)
.default(false),
ext: Validate.object({
onPreAuth: Validate.array().items(internals.event).single(),
onCredentials: Validate.array().items(internals.event).single(),
onPostAuth: Validate.array().items(internals.event).single(),
onPreHandler: Validate.array().items(internals.event).single(),
onPostHandler: Validate.array().items(internals.event).single(),
onPreResponse: Validate.array().items(internals.event).single(),
onPostResponse: Validate.array().items(internals.event).single()
})
.default({}),
files: Validate.object({
relativeTo: Validate.string().pattern(/^([\/\.])|([A-Za-z]:\\)|(\\\\)/).default('.')
})
.default(),
json: Validate.object({
replacer: Validate.alternatives(Validate.function(), Validate.array()).allow(null).default(null),
space: Validate.number().allow(null).default(null),
suffix: Validate.string().allow(null).default(null),
escape: Validate.boolean().default(false)
})
.default(),
log: Validate.object({
collect: Validate.boolean().default(false)
})
.default(),
payload: Validate.object({
output: Validate.valid('data', 'stream', 'file').default('data'),
parse: Validate.boolean().allow('gunzip').default(true),
multipart: Validate.object({
output: Validate.valid('data', 'stream', 'file', 'annotated').required()
})
.default(false)
.allow(true, false),
allow: Validate.array().items(Validate.string()).single(),
override: Validate.string(),
protoAction: Validate.valid('error', 'remove', 'ignore').default('error'),
maxBytes: Validate.number().integer().positive().default(1024 * 1024),
maxParts: Validate.number().integer().positive().default(1000),
uploads: Validate.string().default(Os.tmpdir()),
failAction: internals.failAction,
timeout: Validate.number().integer().positive().allow(false).default(10 * 1000),
defaultContentType: Validate.string().default('application/json'),
compression: Validate.object()
.pattern(/.+/, Validate.object())
.default()
})
.default(),
plugins: Validate.object(),
response: Validate.object({
disconnectStatusCode: Validate.number().integer().min(400).default(499),
emptyStatusCode: Validate.valid(200, 204).default(204),
failAction: internals.failAction,
modify: Validate.boolean(),
options: Validate.object(),
ranges: Validate.boolean().default(true),
sample: Validate.number().min(0).max(100).when('modify', { then: Validate.forbidden() }),
schema: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(true, false),
status: Validate.object().pattern(/\d\d\d/, Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(true, false))
})
.default(),
security: Validate.object({
hsts: Validate.alternatives([
Validate.object({
maxAge: Validate.number(),
includeSubdomains: Validate.boolean(),
includeSubDomains: Validate.boolean(),
preload: Validate.boolean()
}),
Validate.boolean(),
Validate.number()
])
.default(15768000),
xframe: Validate.alternatives([
Validate.boolean(),
Validate.valid('sameorigin', 'deny'),
Validate.object({
rule: Validate.valid('sameorigin', 'deny', 'allow-from'),
source: Validate.string()
})
])
.default('deny'),
xss: Validate.valid('enabled', 'disabled', false).default('disabled'),
noOpen: Validate.boolean().default(true),
noSniff: Validate.boolean().default(true),
referrer: Validate.alternatives([
Validate.boolean().valid(false),
Validate.valid('', 'no-referrer', 'no-referrer-when-downgrade',
'unsafe-url', 'same-origin', 'origin', 'strict-origin',
'origin-when-cross-origin', 'strict-origin-when-cross-origin')
])
.default(false)
})
.allow(null, false, true)
.default(false),
state: Validate.object({
parse: Validate.boolean().default(true),
failAction: internals.failAction
})
.default(),
timeout: Validate.object({
socket: Validate.number().integer().positive().allow(false),
server: Validate.number().integer().positive().allow(false).default(false)
})
.default(),
validate: Validate.object({
headers: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, true),
params: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, true),
query: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
payload: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
state: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
failAction: internals.failAction,
errorFields: Validate.object(),
options: Validate.object().default(),
validator: Validate.object()
})
.default()
});
internals.server = Validate.object({
address: Validate.string().hostname(),
app: Validate.object().allow(null),
autoListen: Validate.boolean(),
cache: Validate.allow(null), // Validated elsewhere
compression: Validate.object({
minBytes: Validate.number().min(1).integer().default(1024)
})
.allow(false)
.default(),
debug: Validate.object({
request: Validate.array().items(Validate.string()).single().allow(false).default(['implementation']),
log: Validate.array().items(Validate.string()).single().allow(false)
})
.allow(false)
.default(),
host: Validate.string().hostname().allow(null),
info: Validate.object({
remote: Validate.boolean().default(false)
})
.default({}),
listener: Validate.any(),
load: Validate.object({
sampleInterval: Validate.number().integer().min(0).default(0)
})
.unknown()
.default(),
mime: Validate.object().empty(null).default(),
operations: Validate.object({
cleanStop: Validate.boolean().default(true)
})
.default(),
plugins: Validate.object(),
port: Validate.alternatives([
Validate.number().integer().min(0), // TCP port
Validate.string().pattern(/\//), // Unix domain socket
Validate.string().pattern(/^\\\\\.\\pipe\\/) // Windows named pipe
])
.allow(null),
query: Validate.object({
parser: Validate.function()
})
.default(),
router: Validate.object({
isCaseSensitive: Validate.boolean().default(true),
stripTrailingSlash: Validate.boolean().default(false)
})
.default(),
routes: internals.routeBase.default(),
state: Validate.object(), // Cookie defaults
tls: Validate.alternatives([
Validate.object().allow(null),
Validate.boolean()
]),
uri: Validate.string().pattern(/[^/]$/)
});
internals.vhost = Validate.alternatives([
Validate.string().hostname(),
Validate.array().items(Validate.string().hostname()).min(1)
]);
internals.handler = Validate.alternatives([
Validate.function(),
Validate.object().length(1)
]);
internals.route = Validate.object({
method: Validate.string().pattern(/^[a-zA-Z0-9!#\$%&'\*\+\-\.^_`\|~]+$/).required(),
path: Validate.string().required(),
rules: Validate.object(),
vhost: internals.vhost,
// Validated in route construction
handler: Validate.any(),
options: Validate.any(),
config: Validate.any() // Backwards compatibility
})
.without('config', 'options');
internals.pre = [
Validate.function(),
Validate.object({
method: Validate.alternatives(Validate.string(), Validate.function()).required(),
assign: Validate.string(),
mode: Validate.valid('serial', 'parallel'),
failAction: internals.failAction
})
];
internals.routeConfig = internals.routeBase.keys({
description: Validate.string(),
id: Validate.string(),
isInternal: Validate.boolean(),
notes: [
Validate.string(),
Validate.array().items(Validate.string())
],
pre: Validate.array().items(...internals.pre.concat(Validate.array().items(...internals.pre).min(1))),
tags: [
Validate.string(),
Validate.array().items(Validate.string())
]
});
internals.cacheConfig = Validate.alternatives([
Validate.function(),
Validate.object({
name: Validate.string().invalid('_default'),
shared: Validate.boolean(),
provider: [
Validate.function(),
{
constructor: Validate.function().required(),
options: Validate.object({
partition: Validate.string().default('hapi-cache')
})
.unknown() // Catbox client validates other keys
.default({})
}
],
engine: Validate.object()
})
.xor('provider', 'engine')
]);
internals.cache = Validate.array().items(internals.cacheConfig).min(1).single();
internals.cachePolicy = Validate.object({
cache: Validate.string().allow(null).allow(''),
segment: Validate.string(),
shared: Validate.boolean()
})
.unknown(); // Catbox policy validates other keys
internals.method = Validate.object({
bind: Validate.object().allow(null),
generateKey: Validate.function(),
cache: internals.cachePolicy
});
internals.methodObject = Validate.object({
name: Validate.string().required(),
method: Validate.function().required(),
options: Validate.object()
});
internals.register = Validate.object({
once: true,
routes: Validate.object({
prefix: Validate.string().pattern(/^\/.+/),
vhost: internals.vhost
})
.default({})
});
internals.semver = Validate.string();
internals.plugin = internals.register.keys({
options: Validate.any(),
plugin: Validate.object({
register: Validate.function().required(),
name: Validate.string().when('pkg.name', { is: Validate.exist(), otherwise: Validate.required() }),
version: Validate.string(),
multiple: Validate.boolean().default(false),
dependencies: [
Validate.array().items(Validate.string()).single(),
Validate.object().pattern(/.+/, internals.semver)
],
once: true,
requirements: Validate.object({
hapi: Validate.string(),
node: Validate.string()
})
.default(),
pkg: Validate.object({
name: Validate.string(),
version: Validate.string().default('0.0.0')
})
.unknown()
.default({})
})
.unknown()
})
.without('once', 'options')
.unknown();
internals.rules = Validate.object({
validate: Validate.object({
schema: Validate.alternatives(Validate.object(), Validate.array()).required(),
options: Validate.object()
.default({ allowUnknown: true })
})
});
================================================
FILE: lib/core.js
================================================
'use strict';
const Http = require('http');
const Https = require('https');
const Os = require('os');
const Path = require('path');
const Boom = require('@hapi/boom');
const Bounce = require('@hapi/bounce');
const Call = require('@hapi/call');
const Catbox = require('@hapi/catbox');
const { Engine: CatboxMemory } = require('@hapi/catbox-memory');
const { Heavy } = require('@hapi/heavy');
const Hoek = require('@hapi/hoek');
const { Mimos } = require('@hapi/mimos');
const Podium = require('@hapi/podium');
const Statehood = require('@hapi/statehood');
const Auth = require('./auth');
const Compression = require('./compression');
const Config = require('./config');
const Cors = require('./cors');
const Ext = require('./ext');
const Methods = require('./methods');
const Request = require('./request');
const Response = require('./response');
const Route = require('./route');
const Toolkit = require('./toolkit');
const Validation = require('./validation');
const internals = {
counter: {
min: 10000,
max: 99999
},
events: [
{ name: 'cachePolicy', spread: true },
{ name: 'log', channels: ['app', 'internal'], tags: true },
{ name: 'request', channels: ['app', 'internal', 'error'], tags: true, spread: true },
'response',
'route',
'start',
'closing',
'stop'
],
badRequestResponse: Buffer.from('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii')
};
exports = module.exports = internals.Core = class {
actives = new WeakMap(); // Active requests being processed
app = {};
auth = new Auth(this);
caches = new Map(); // Cache clients
compression = new Compression();
controlled = null; // Other servers linked to the phases of this server
dependencies = []; // Plugin dependencies
events = new Podium.Podium(internals.events);
heavy = null;
info = null;
instances = new Set();
listener = null;
methods = new Methods(this); // Server methods
mime = null;
onConnection = null; // Used to remove event listener on stop
phase = 'stopped'; // 'stopped', 'initializing', 'initialized', 'starting', 'started', 'stopping', 'invalid'
plugins = {}; // Exposed plugin properties by name
registrations = {}; // Tracks plugin for dependency validation { name -> { version } }
registring = 0; // > 0 while register() is waiting for plugin callbacks
Request = class extends Request { };
Response = class extends Response { };
requestCounter = { value: internals.counter.min, min: internals.counter.min, max: internals.counter.max };
root = null;
router = null;
settings = null;
sockets = null; // Track open sockets for graceful shutdown
started = false;
states = null;
toolkit = new Toolkit.Manager();
type = null;
validator = null;
extensionsSeq = 0; // Used to keep absolute order of extensions based on the order added across locations
extensions = {
server: {
onPreStart: new Ext('onPreStart', this),
onPostStart: new Ext('onPostStart', this),
onPreStop: new Ext('onPreStop', this),
onPostStop: new Ext('onPostStop', this)
},
route: {
onRequest: new Ext('onRequest', this),
onPreAuth: new Ext('onPreAuth', this),
onCredentials: new Ext('onCredentials', this),
onPostAuth: new Ext('onPostAuth', this),
onPreHandler: new Ext('onPreHandler', this),
onPostHandler: new Ext('onPostHandler', this),
onPreResponse: new Ext('onPreResponse', this),
onPostResponse: new Ext('onPostResponse', this)
}
};
decorations = {
handler: new Map(),
request: new Map(),
response: new Map(),
server: new Map(),
toolkit: new Map(),
requestApply: null,
public: { handler: [], request: [], response: [], server: [], toolkit: [] }
};
constructor(options) {
const { settings, type } = internals.setup(options);
this.settings = settings;
this.type = type;
this.heavy = new Heavy(this.settings.load);
this.mime = new Mimos(this.settings.mime);
this.router = new Call.Router(this.settings.router);
this.states = new Statehood.Definitions(this.settings.state);
this._debug();
this._initializeCache();
if (this.settings.routes.validate.validator) {
this.validator = Validation.validator(this.settings.routes.validate.validator);
}
this.listener = this._createListener();
this._initializeListener();
this.info = this._info();
}
_debug() {
const debug = this.settings.debug;
if (!debug) {
return;
}
// Subscribe to server log events
const method = (event) => {
const data = event.error ?? event.data;
console.error('Debug:', event.tags.join(', '), data ? '\n ' + (data.stack ?? (typeof data === 'object' ? Hoek.stringify(data) : data)) : '');
};
if (debug.log) {
const filter = debug.log.some((tag) => tag === '*') ? undefined : debug.log;
this.events.on({ name: 'log', filter }, method);
}
if (debug.request) {
const filter = debug.request.some((tag) => tag === '*') ? undefined : debug.request;
this.events.on({ name: 'request', filter }, (request, event) => method(event));
}
}
_initializeCache() {
if (this.settings.cache) {
this._createCache(this.settings.cache);
}
if (!this.caches.has('_default')) {
this._createCache([{ provider: CatboxMemory }]); // Defaults to memory-based
}
}
_info() {
const now = Date.now();
const protocol = this.type === 'tcp' ? (this.settings.tls ? 'https' : 'http') : this.type;
const host = this.settings.host || Os.hostname() || 'localhost';
const port = this.settings.port;
const info = {
created: now,
started: 0,
host,
port,
protocol,
id: Os.hostname() + ':' + process.pid + ':' + now.toString(36),
uri: this.settings.uri ?? (protocol + ':' + (this.type === 'tcp' ? '//' + host + (port ? ':' + port : '') : port))
};
return info;
}
_counter() {
const next = ++this.requestCounter.value;
if (this.requestCounter.value > this.requestCounter.max) {
this.requestCounter.value = this.requestCounter.min;
}
return next - 1;
}
_createCache(configs) {
Hoek.assert(this.phase !== 'initializing', 'Cannot provision server cache while server is initializing');
configs = Config.apply('cache', configs);
const added = [];
for (let config of configs) {
// (type: 'handler', property: P, method: HandlerDecorationMethod, options?: { apply?: boolean | undefined, extend?: never }): void; decorate
(type: 'request', property: ExceptName
, method: (existing: ((...args: any[]) => any)) => (request: Request) => DecorationMethod (type: 'request', property: ExceptName , method: (request: Request) => DecorationMethod (type: 'request', property: ExceptName , method: DecorationMethod (type: 'request', property: ExceptName , value: (existing: ((...args: any[]) => any)) => (request: Request) => any, options: {apply: true, extend: true}): void;
decorate (type: 'request', property: ExceptName , value: (request: Request) => any, options: {apply: true, extend?: boolean | undefined}): void;
decorate (type: 'request', property: ExceptName , value: DecorationValue, options?: never): void;
decorate (type: 'toolkit', property: ExceptName , method: (existing: ((...args: any[]) => any)) => DecorationMethod (type: 'toolkit', property: ExceptName , method: DecorationMethod (type: 'toolkit', property: ExceptName , value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
decorate (type: 'toolkit', property: ExceptName , value: DecorationValue, options?: never): void;
decorate (type: 'server', property: ExceptName , method: (existing: ((...args: any[]) => any)) => DecorationMethod (type: 'server', property: ExceptName , method: DecorationMethod (type: 'server', property: ExceptName , value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
decorate (type: 'server', property: ExceptName , value: DecorationValue, options?: never): void;
/**
* Used within a plugin to declare a required dependency on other plugins where:
* @param dependencies - plugins which must be registered in order for this plugin to operate. Plugins listed must be registered before the server is
* initialized or started.
* @param after - (optional) a function that is called after all the specified dependencies have been registered and before the server starts. The function is only called if the server is
* initialized or started. The function signature is async function(server) where: server - the server the dependency() method was called on.
* @return Return value: none.
* The after method is identical to setting a server extension point on 'onPreStart'.
* If a circular dependency is detected, an exception is thrown (e.g. two plugins each has an after function to be called after the other).
* The method does not provide version dependency which should be implemented using npm peer dependencies.
* [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverdependencydependencies-after)
*/
dependency(dependencies: Dependencies, after?: ((server: Server) => Promisesteve
');
const res2 = await server.inject('/');
expect(res2.statusCode).to.equal(200);
expect(res2.result).to.equal('xyz
');
});
it('exposes an api', () => {
const implementation = function (server, options) {
return {
api: {
x: 5
},
authenticate: (request, h) => h.continue(null, {})
};
};
const server = Hapi.server();
server.auth.scheme('custom', implementation);
server.auth.strategy('xyz', 'custom');
server.auth.default('xyz');
expect(server.auth.api.xyz.x).to.equal(5);
});
it('has its own realm', async () => {
const implementation = function (server) {
return {
authenticate: (_, h) => h.authenticated({ credentials: server.realm })
};
};
const server = Hapi.server();
server.auth.scheme('custom', implementation);
server.auth.strategy('root', 'custom');
let pluginA;
await server.register({
name: 'plugin-a',
register(srv) {
pluginA = srv;
srv.auth.strategy('a', 'custom');
}
});
const handler = (request) => request.auth.credentials;
server.route({ method: 'GET', path: '/a', handler, options: { auth: 'a' } });
server.route({ method: 'GET', path: '/root', handler, options: { auth: 'root' } });
const { result: realm1 } = await server.inject('/a');
expect(realm1.plugin).to.be.undefined();
expect(realm1).to.not.shallow.equal(server.realm);
expect(realm1.parent).to.shallow.equal(pluginA.realm);
const { result: realm2 } = await server.inject('/root');
expect(realm2.plugin).to.be.undefined();
expect(realm2).to.not.shallow.equal(server.realm);
expect(realm2.parent).to.shallow.equal(server.realm);
});
});
describe('default()', () => {
it('sets default', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.auth.default('default');
expect(server.auth.settings.default).to.equal({ strategies: ['default'], mode: 'required' });
server.route({ method: 'GET', path: '/', handler: (request) => request.auth.credentials.user });
const res1 = await server.inject('/');
expect(res1.statusCode).to.equal(401);
const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res2.statusCode).to.equal(204);
});
it('sets default with object', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.auth.default({ strategy: 'default' });
expect(server.auth.settings.default).to.equal({ strategies: ['default'], mode: 'required' });
server.route({ method: 'GET', path: '/', handler: (request) => request.auth.credentials.user });
const res1 = await server.inject('/');
expect(res1.statusCode).to.equal(401);
const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res2.statusCode).to.equal(204);
});
it('throws when setting default twice', () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
expect(() => {
server.auth.default('default');
server.auth.default('default');
}).to.throw('Cannot set default strategy more than once');
});
it('throws when setting default without strategy', () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
expect(() => {
server.auth.default({ mode: 'required' });
}).to.throw('Missing authentication strategy: default strategy');
});
it('matches dynamic scope', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve', scope: 'one-test-admin-x-steve' } } });
server.auth.default({ strategy: 'default', scope: 'one-{params.id}-{params.role}-{payload.x}-{credentials.user}' });
server.route({
method: 'POST',
path: '/{id}/{role}',
handler: (request) => request.auth.credentials.user
});
const res = await server.inject({ method: 'POST', url: '/test/admin', headers: { authorization: 'Custom steve' }, payload: { x: 'x' } });
expect(res.statusCode).to.equal(200);
});
});
describe('_setupRoute()', () => {
it('throws when route refers to nonexistent strategy', () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('a', 'custom', { users: { steve: {} } });
server.auth.strategy('b', 'custom', { users: { steve: {} } });
expect(() => {
server.route({
path: '/',
method: 'GET',
options: {
auth: {
strategy: 'c'
},
handler: () => 'ok'
}
});
}).to.throw('Unknown authentication strategy c in /');
});
});
describe('lookup', () => {
it('returns the route auth config', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.auth.default('default');
server.route({ method: 'GET', path: '/', handler: (request) => request.server.auth.lookup(request.route) });
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal({
strategies: ['default'],
mode: 'required'
});
});
});
describe('authenticate()', () => {
it('setups route with optional authentication', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => !!request.auth.credentials,
auth: {
mode: 'optional'
}
}
});
const res1 = await server.inject('/');
expect(res1.statusCode).to.equal(200);
expect(res1.payload).to.equal('false');
const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res2.statusCode).to.equal(200);
expect(res2.payload).to.equal('true');
});
it('exposes mode', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
handler: (request) => request.auth.mode
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('required');
});
it('authenticates using multiple strategies', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('first', 'custom', { users: { steve: 'skip' } });
server.auth.strategy('second', 'custom', { users: { steve: {} } });
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.strategy,
auth: {
strategies: ['first', 'second']
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('second');
});
it('authenticates using credentials object', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });
server.auth.default('default');
const doubleHandler = async (request) => {
const options = { url: '/2', auth: { credentials: request.auth.credentials, strategy: 'default' } };
const res = await server.inject(options);
return res.result;
};
server.route({ method: 'GET', path: '/1', handler: doubleHandler });
server.route({ method: 'GET', path: '/2', handler: (request) => request.auth.credentials.user });
const res = await server.inject({ url: '/1', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('steve');
});
it('authenticates using credentials object (with artifacts)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });
server.auth.default('default');
const doubleHandler = async (request) => {
const options = { url: '/2', auth: { credentials: request.auth.credentials, artifacts: '!', strategy: 'default' } };
const res = await server.inject(options);
return res.result;
};
const handler = (request) => {
return request.auth.credentials.user + request.auth.artifacts;
};
server.route({ method: 'GET', path: '/1', handler: doubleHandler });
server.route({ method: 'GET', path: '/2', handler });
const res = await server.inject({ url: '/1', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('steve!');
});
it('authenticates a request with custom auth settings', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
strategy: 'default'
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('authenticates a request with auth strategy name config', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: 'default'
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('tries to authenticate a request', async () => {
const handler = (request) => {
return { status: request.auth.isAuthenticated, error: request.auth.error };
};
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.auth.default({ strategy: 'default', mode: 'try' });
server.route({ method: 'GET', path: '/', handler });
const res1 = await server.inject('/');
expect(res1.statusCode).to.equal(200);
expect(res1.result.status).to.equal(false);
expect(res1.result.error.message).to.equal('Missing authentication');
const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
expect(res2.statusCode).to.equal(200);
expect(res2.result.status).to.equal(false);
expect(res2.result.error.message).to.equal('Missing credentials');
const res3 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res3.statusCode).to.equal(200);
expect(res3.result.status).to.equal(true);
expect(res3.result.error).to.not.exist();
});
it('errors on invalid authenticate callback missing both error and credentials', async () => {
const server = Hapi.server({ debug: false });
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.auth.default('default');
server.route({ method: 'GET', path: '/', handler: (request) => request.auth.credentials.user });
const res = await server.inject({ url: '/', headers: { authorization: 'Custom' } });
expect(res.statusCode).to.equal(500);
});
it('logs error', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.auth.default('default');
server.route({ method: 'GET', path: '/', handler: (request) => request.auth.credentials.user });
let logged = false;
server.events.on({ name: 'request', channels: 'internal' }, (request, event, tags) => {
if (tags.auth) {
logged = true;
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
expect(res.statusCode).to.equal(401);
expect(logged).to.be.true();
});
it('returns a non Error error response', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { message: 'in a bottle' } });
server.auth.default('default');
server.route({ method: 'GET', path: '/', handler: (request) => request.auth.credentials.user });
const res = await server.inject({ url: '/', headers: { authorization: 'Custom message' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('in a bottle');
});
it('passes non Error error response when set to try ', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { message: 'in a bottle' } });
server.auth.default({ strategy: 'default', mode: 'try' });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const res = await server.inject({ url: '/', headers: { authorization: 'Custom message' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('in a bottle');
});
it('matches scope (array to single)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: ['one'] } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: 'one'
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('matches scope (array to array)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: ['one', 'two'] } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: ['one', 'three']
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('matches scope (single to array)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: 'one' } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: ['one', 'three']
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('matches scope (single to single)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: 'one' } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: 'one'
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('matches dynamic scope (single to single)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: 'one-test' } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/{id}',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: 'one-{params.id}'
}
}
});
const res = await server.inject({ url: '/test', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('matches multiple required dynamic scopes', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: ['test', 'one-test'] } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/{id}',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: ['+one-{params.id}', '+{params.id}']
}
}
});
const res = await server.inject({ url: '/test', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('matches multiple required dynamic scopes (mixed types)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: ['test', 'one-test'] } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/{id}',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: ['+one-{params.id}', '{params.id}']
}
}
});
const res = await server.inject({ url: '/test', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('matches dynamic scope with multiple parts (single to single)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: 'one-test-admin' } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/{id}/{role}',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: 'one-{params.id}-{params.role}'
}
}
});
const res = await server.inject({ url: '/test/admin', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('does not match broken dynamic scope (single to single)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: 'one-test' } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/{id}',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: 'one-params.id}'
}
}
});
server.ext('onPreResponse', (request, h) => {
expect(request.response.data).to.contain(['got', 'need']);
return h.continue;
});
const res = await server.inject({ url: '/test', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(403);
expect(res.result.message).to.equal('Insufficient scope');
});
it('does not match scope (single to single)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: 'one' } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: 'onex'
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(403);
expect(res.result.message).to.equal('Insufficient scope');
});
it('matches modified scope', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: 'two' } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: 'one'
}
}
});
server.ext('onCredentials', (request, h) => {
request.auth.credentials.scope = 'one';
return h.continue;
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('errors on missing scope', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: ['a'] } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: 'b'
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(403);
expect(res.result.message).to.equal('Insufficient scope');
});
it('errors on missing scope property', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: 'b'
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(403);
expect(res.result.message).to.equal('Insufficient scope');
});
it('validates required scope', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', {
users: {
steve: { scope: ['a', 'b'] },
john: { scope: ['a', 'b', 'c'] }
}
});
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: ['+c', 'b']
}
}
});
const res1 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res1.statusCode).to.equal(403);
expect(res1.result.message).to.equal('Insufficient scope');
const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
expect(res2.statusCode).to.equal(204);
});
it('validates forbidden scope', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', {
users: {
steve: { scope: ['a', 'b'] },
john: { scope: ['b', 'c'] }
}
});
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: ['!a', 'b']
}
}
});
const res1 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res1.statusCode).to.equal(403);
expect(res1.result.message).to.equal('Insufficient scope');
const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
expect(res2.statusCode).to.equal(204);
});
it('validates complex scope', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', {
users: {
steve: { scope: ['a', 'b', 'c'] },
john: { scope: ['b', 'c'] },
mary: { scope: ['b', 'd'] },
lucy: { scope: 'b' },
larry: { scope: ['c', 'd'] }
}
});
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: ['!a', '+b', 'c', 'd']
}
}
});
const res1 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res1.statusCode).to.equal(403);
expect(res1.result.message).to.equal('Insufficient scope');
const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
expect(res2.statusCode).to.equal(204);
const res3 = await server.inject({ url: '/', headers: { authorization: 'Custom mary' } });
expect(res3.statusCode).to.equal(204);
const res4 = await server.inject({ url: '/', headers: { authorization: 'Custom lucy' } });
expect(res4.statusCode).to.equal(403);
expect(res4.result.message).to.equal('Insufficient scope');
const res5 = await server.inject({ url: '/', headers: { authorization: 'Custom larry' } });
expect(res5.statusCode).to.equal(403);
expect(res5.result.message).to.equal('Insufficient scope');
});
it('errors on missing scope using arrays', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: ['a', 'b'] } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: ['c', 'd']
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(403);
expect(res.result.message).to.equal('Insufficient scope');
});
it('uses default scope when no scope override is set', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('a', 'custom', { users: { steve: { scope: ['two'] } } });
server.auth.default({
strategy: 'a',
access: {
scope: 'one'
}
});
server.route({
path: '/',
method: 'GET',
options: {
auth: {
mode: 'required'
},
handler: () => 'ok'
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(403);
expect(res.result.message).to.equal('Insufficient scope');
});
it('ignores default scope when override set to null', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.auth.default({
strategy: 'default',
scope: 'one'
});
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
scope: false
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('matches scope (access single)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: ['one'] } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth,
auth: {
access: {
scope: 'one'
}
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal({
isAuthenticated: true,
isAuthorized: true,
isInjected: false,
credentials: { scope: ['one'], user: null },
artifacts: undefined,
strategy: 'default',
mode: 'required',
error: null
}, { symbols: false });
});
it('matches scope (access array)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: ['one'] } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
access: [
{ scope: 'other' },
{ scope: 'one' }
]
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('errors on matching scope (access array)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: ['one'] } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
access: [
{ scope: 'two' },
{ scope: 'three' },
{ entity: 'user', scope: 'one' },
{ entity: 'app', scope: 'four' }
]
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(403);
expect(res.result.message).to.equal('Insufficient scope');
});
it('matches any entity', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: () => null,
auth: {
entity: 'any'
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('matches user entity', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: () => null,
auth: {
entity: 'user'
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(204);
});
it('errors on missing user entity', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { client: {} } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: () => null,
auth: {
entity: 'user'
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom client' } });
expect(res.statusCode).to.equal(403);
expect(res.result.message).to.equal('Application credentials cannot be used on a user endpoint');
});
it('matches app entity', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { client: {} } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: () => null,
auth: {
entity: 'app'
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom client' } });
expect(res.statusCode).to.equal(204);
});
it('errors on missing app entity', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: () => null,
auth: {
entity: 'app'
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(403);
expect(res.result.message).to.equal('User credentials cannot be used on an application endpoint');
});
it('logs error code when authenticate returns a non-error error', async () => {
const server = Hapi.server();
server.auth.scheme('test', (srv, options) => {
return {
authenticate: (request, h) => h.response('Redirecting ...').redirect('/test').takeover()
};
});
server.auth.strategy('test', 'test', {});
server.auth.default('test');
server.route({
method: 'GET',
path: '/',
handler: () => 'test'
});
let logged = null;
server.events.on({ name: 'request', channels: 'internal' }, (request, event, tags) => {
if (tags.unauthenticated) {
logged = event;
}
});
await server.inject('/');
expect(logged.data).to.equal({ statusCode: 302 });
});
it('passes the options.artifacts object, even with an auth filter', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth.artifacts,
auth: 'default'
}
});
const options = {
url: '/',
headers: { authorization: 'Custom steve' },
auth: {
credentials: { foo: 'bar' },
artifacts: { bar: 'baz' },
strategy: 'default'
}
};
const res = await server.inject(options);
expect(res.statusCode).to.equal(200);
expect(res.result.bar).to.equal('baz');
});
it('errors on empty authenticate()', async () => {
const scheme = () => {
return { authenticate: (request, h) => h.authenticated() };
};
const server = Hapi.server({ debug: false });
server.auth.scheme('custom', scheme);
server.auth.strategy('default', 'custom');
server.auth.default('default');
server.route({ method: 'GET', path: '/', handler: () => null });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
it('passes credentials on unauthenticated() in try mode', async () => {
const scheme = () => {
return { authenticate: (request, h) => h.unauthenticated(Boom.unauthorized(), { credentials: { user: 'steve' } }) };
};
const server = Hapi.server();
server.ext('onPreResponse', (request, h) => {
if (request.auth.credentials.user === 'steve') {
return h.continue;
}
});
server.auth.scheme('custom', scheme);
server.auth.strategy('default', 'custom');
server.auth.default({ strategy: 'default', mode: 'try' });
server.route({ method: 'GET', path: '/', handler: () => null });
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
});
it('passes strategy, credentials, artifacts, error on unauthenticated() in required mode', async () => {
const scheme = () => {
return { authenticate: (request, h) => h.unauthenticated(Boom.unauthorized(), { credentials: { user: 'steve' }, artifacts: '!' }) };
};
const server = Hapi.server();
server.ext('onPreResponse', (request, h) => {
if (request.auth.credentials.user === 'steve') {
return h.continue;
}
});
server.ext('onPreResponse', (request, h) => {
expect(request.auth.credentials).to.equal({ user: 'steve' });
expect(request.auth.artifacts).to.equal('!');
expect(request.auth.strategy).to.equal('default');
expect(request.auth.error.message).to.equal('Unauthorized');
return h.continue;
});
server.auth.scheme('custom', scheme);
server.auth.strategy('default', 'custom');
server.auth.default('default', { mode: 'required' });
server.route({ method: 'GET', path: '/', handler: () => null });
const res = await server.inject('/');
expect(res.statusCode).to.equal(401);
});
});
describe('verify()', () => {
it('verifies an authenticated request', async () => {
const implementation = (...args) => {
const imp = internals.implementation(...args);
imp.verify = async (auth) => {
await Hoek.wait(1);
if (auth.credentials.user !== 'steve') {
throw Boom.unauthorized('Invalid');
}
};
return imp;
};
const server = Hapi.server();
server.auth.scheme('custom', implementation);
server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' }, john: { user: 'john' } } });
server.route({
method: 'GET',
path: '/',
options: {
auth: {
mode: 'try',
strategy: 'default'
},
handler: async (request) => {
if (request.auth.error &&
request.auth.error.message === 'Missing authentication') {
request.auth.error = null;
}
return await server.auth.verify(request) || 'ok';
}
}
});
const res1 = await server.inject('/');
expect(res1.result).to.equal('ok');
const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res2.result).to.equal('ok');
const res3 = await server.inject({ url: '/', headers: { authorization: 'Custom unknown' } });
expect(res3.result.message).to.equal('Missing credentials');
const res4 = await server.inject({ url: '/', auth: { credentials: {}, strategy: 'default' } });
expect(res4.result.message).to.equal('Invalid');
const res5 = await server.inject({ url: '/', auth: { credentials: { user: 'steve' }, strategy: 'default' } });
expect(res5.result).to.equal('ok');
const res6 = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
expect(res6.result.message).to.equal('Invalid');
});
it('skips when verify unsupported', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });
server.route({
method: 'GET',
path: '/',
options: {
auth: {
mode: 'try',
strategy: 'default'
},
handler: async (request) => {
return await server.auth.verify(request) || 'ok';
}
}
});
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.result).to.equal('ok');
});
});
describe('access()', () => {
it('skips access when unauthenticated and mode is not required', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { scope: ['one'] } } });
server.auth.default('default');
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.auth,
auth: {
mode: 'optional',
access: {
scope: 'one'
}
}
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result.isAuthenticated).to.be.false();
expect(res.result.isAuthorized).to.be.false();
});
});
describe('payload()', () => {
it('authenticates request payload', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { validPayload: { payload: null } } });
server.auth.default('default');
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
payload: 'required'
}
}
});
const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom validPayload' } });
expect(res.statusCode).to.equal(204);
});
it('skips when scheme does not support it', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { validPayload: { payload: null } }, payload: false });
server.auth.default('default');
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user
}
});
const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom validPayload' } });
expect(res.statusCode).to.equal(204);
});
it('authenticates request payload (required scheme)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { validPayload: { payload: null } }, options: { payload: true } });
server.auth.default('default');
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {}
}
});
const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom validPayload' } });
expect(res.statusCode).to.equal(204);
});
it('authenticates request payload (required scheme and required route)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { validPayload: { payload: null } }, options: { payload: true } });
server.auth.default('default');
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
payload: true
}
}
});
const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom validPayload' } });
expect(res.statusCode).to.equal(204);
});
it('throws when scheme requires payload authentication and route conflicts', () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { validPayload: { payload: null } }, options: { payload: true } });
server.auth.default('default');
expect(() => {
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
payload: 'optional'
}
}
});
}).to.throw('Cannot set authentication payload to optional when a strategy requires payload validation in /');
});
it('throws when strategy does not support payload authentication', () => {
const server = Hapi.server();
const implementation = function () {
return { authenticate: internals.implementation().authenticate };
};
server.auth.scheme('custom', implementation);
server.auth.strategy('default', 'custom', {});
server.auth.default('default');
expect(() => {
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
payload: 'required'
}
}
});
}).to.throw('Payload validation can only be required when all strategies support it in /');
});
it('throws when no strategy supports optional payload authentication', () => {
const server = Hapi.server();
const implementation = function () {
return { authenticate: internals.implementation().authenticate };
};
server.auth.scheme('custom', implementation);
server.auth.strategy('default', 'custom', {});
server.auth.default('default');
expect(() => {
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
payload: 'optional'
}
}
});
}).to.throw('Payload authentication requires at least one strategy with payload support in /');
});
it('allows one strategy to supports optional payload authentication while another does not', async () => {
const server = Hapi.server();
const implementation = function (...args) {
return { authenticate: internals.implementation(...args).authenticate };
};
server.auth.scheme('custom1', implementation);
server.auth.scheme('custom2', internals.implementation, { users: {} });
server.auth.strategy('default1', 'custom1', { users: { steve: { user: 'steve' } } });
server.auth.strategy('default2', 'custom2', {});
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
strategies: ['default1', 'default2'],
payload: 'optional'
}
}
});
const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(200);
});
it('skips request payload by default', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { skip: {} } });
server.auth.default('default');
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user
}
});
const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom skip' } });
expect(res.statusCode).to.equal(204);
});
it('skips request payload when unauthenticated', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { skip: {} } });
server.auth.default('default');
server.route({
method: 'POST',
path: '/',
options: {
handler: () => null,
auth: {
mode: 'try',
payload: 'required'
}
}
});
const res = await server.inject({ method: 'POST', url: '/' });
expect(res.statusCode).to.equal(204);
});
it('skips optional payload', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { optionalPayload: { payload: Boom.unauthorized(null, 'Custom') } } });
server.auth.default('default');
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
payload: 'optional'
}
}
});
const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom optionalPayload' } });
expect(res.statusCode).to.equal(204);
});
it('skips required payload authentication when disabled on injection', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom');
server.auth.default('default');
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => null,
auth: {
mode: 'try',
payload: true
}
}
});
const res = await server.inject({ method: 'POST', url: '/', auth: { credentials: { payload: Boom.internal('payload error') }, payload: false, strategy: 'default' } });
expect(res.statusCode).to.equal(204);
});
it('errors on missing payload when required', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { optionalPayload: { payload: Boom.unauthorized(null, 'Custom') } } });
server.auth.default('default');
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
payload: 'required'
}
}
});
const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom optionalPayload' } });
expect(res.statusCode).to.equal(401);
});
it('errors on invalid payload auth when required', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { optionalPayload: { payload: Boom.unauthorized() } } });
server.auth.default('default');
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
payload: 'required'
}
}
});
const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom optionalPayload' } });
expect(res.statusCode).to.equal(401);
});
it('errors on invalid request payload (non error)', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { invalidPayload: { payload: 'Payload is invalid' } } });
server.auth.default('default');
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
payload: 'required'
}
}
});
const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom invalidPayload' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('Payload is invalid');
});
});
describe('response()', () => {
it('fails on response error', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { response: Boom.internal() } } });
server.auth.default('default');
server.route({ method: 'GET', path: '/', handler: (request) => request.auth.credentials.user });
const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.statusCode).to.equal(500);
});
});
describe('test()', () => {
it('tests a request', async () => {
const handler = async (request) => {
try {
const { credentials, artifacts } = await request.server.auth.test('default', request);
return { status: true, user: credentials.name, artifacts };
}
catch (err) {
return { status: false };
}
};
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { name: 'steve' }, skip: 'skip' }, artifacts: {} });
server.route({ method: 'GET', path: '/', handler });
const res1 = await server.inject('/');
expect(res1.statusCode).to.equal(200);
expect(res1.result.status).to.be.false();
const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res2.statusCode).to.equal(200);
expect(res2.result.status).to.be.true();
expect(res2.result.user).to.equal('steve');
expect(res2.result.artifacts).to.equal({});
const res3 = await server.inject({ url: '/', headers: { authorization: 'Custom skip' } });
expect(res3.statusCode).to.equal(200);
expect(res3.result.status).to.be.false();
});
});
});
internals.implementation = function (server, options) {
const settings = Hoek.clone(options);
if (settings &&
settings.route) {
server.route({ method: 'GET', path: '/', handler: (request) => (request.auth.credentials.user || null) });
}
const scheme = {
authenticate: (request, h) => {
const req = request.raw.req;
const authorization = req.headers.authorization;
if (!authorization) {
return Boom.unauthorized(null, 'Custom');
}
const parts = authorization.split(/\s+/);
if (parts.length !== 2) {
return h.continue; // Error without error or credentials
}
const username = parts[1];
const credentials = settings.users[username];
if (!credentials) {
throw Boom.unauthorized('Missing credentials', 'Custom');
}
if (credentials === 'skip') {
return h.unauthenticated(Boom.unauthorized(null, 'Custom'));
}
if (typeof credentials === 'string') {
return h.response(credentials).takeover();
}
credentials.user = credentials.user || null;
return h.authenticated({ credentials, artifacts: settings.artifacts });
},
response: (request, h) => {
if (request.auth.credentials.response) {
throw request.auth.credentials.response;
}
return h.continue;
}
};
if (!settings ||
settings.payload !== false) {
scheme.payload = (request, h) => {
const result = request.auth.credentials.payload;
if (!result) {
return h.continue;
}
if (result.isBoom) {
throw result;
}
return h.response(request.auth.credentials.payload).takeover();
};
}
if (settings &&
settings.options) {
scheme.options = settings.options;
}
return scheme;
};
================================================
FILE: test/common.js
================================================
'use strict';
const ChildProcess = require('child_process');
const Http = require('http');
const Net = require('net');
const internals = {};
internals.hasLsof = () => {
try {
ChildProcess.execSync(`lsof -p ${process.pid}`, { stdio: 'ignore' });
}
catch (err) {
return false;
}
return true;
};
internals.hasIPv6 = () => {
const server = Http.createServer().listen();
const { address } = server.address();
server.close();
return Net.isIPv6(address);
};
exports.hasLsof = internals.hasLsof();
exports.hasIPv6 = internals.hasIPv6();
================================================
FILE: test/core.js
================================================
'use strict';
const ChildProcess = require('child_process');
const Events = require('events');
const Fs = require('fs');
const Http = require('http');
const Https = require('https');
const Net = require('net');
const Os = require('os');
const Path = require('path');
const Stream = require('stream');
const TLS = require('tls');
const Boom = require('@hapi/boom');
const { Engine: CatboxMemory } = require('@hapi/catbox-memory');
const Code = require('@hapi/code');
const Handlebars = require('handlebars');
const Hapi = require('..');
const Hoek = require('@hapi/hoek');
const Inert = require('@hapi/inert');
const Lab = require('@hapi/lab');
const Teamwork = require('@hapi/teamwork');
const Vision = require('@hapi/vision');
const Wreck = require('@hapi/wreck');
const Common = require('./common');
const internals = {};
const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;
describe('Core', () => {
it('sets app settings defaults', () => {
const server = Hapi.server();
expect(server.settings.app).to.equal({});
});
it('sets app settings', () => {
const server = Hapi.server({ app: { message: 'test defaults' } });
expect(server.settings.app.message).to.equal('test defaults');
});
it('overrides mime settings', () => {
const options = {
mime: {
override: {
'node/module': {
source: 'steve',
compressible: false,
extensions: ['node', 'module', 'npm'],
type: 'node/module'
}
}
}
};
const server = Hapi.server(options);
expect(server.mime.path('file.npm').type).to.equal('node/module');
expect(server.mime.path('file.npm').source).to.equal('steve');
});
it('allows null port and host', () => {
expect(() => {
Hapi.server({ host: null, port: null });
}).to.not.throw();
});
it('does not throw when given a default authentication strategy', () => {
expect(() => {
Hapi.server({ routes: { auth: 'test' } });
}).not.to.throw();
});
it('throws when disabling autoListen and providing a port', () => {
expect(() => {
Hapi.server({ port: 80, autoListen: false });
}).to.throw('Cannot specify port when autoListen is false');
});
it('throws when disabling autoListen and providing special host', () => {
expect(() => {
Hapi.server({ port: '/a/b/hapi-server.socket', autoListen: false });
}).to.throw('Cannot specify port when autoListen is false');
});
it('defaults address to 0.0.0.0 or :: when no host is provided', async (flags) => {
const server = Hapi.server();
await server.start();
flags.onCleanup = () => server.stop();
const expectedBoundAddress = Common.hasIPv6 ? '::' : '0.0.0.0';
expect(server.info.address).to.equal(expectedBoundAddress);
});
it('is accessible on localhost when using default host', async (flags) => {
// With hapi v20 this would fail on ipv6 machines on node v18+ due to DNS resolution changes in node (see nodejs/node#40537).
// To address this in hapi v21 we bind to :: if available, otherwise the former default of 0.0.0.0.
const server = Hapi.server();
server.route({ method: 'get', path: '/', handler: () => 'ok' });
await server.start();
flags.onCleanup = () => server.stop();
const req = Http.get(`http://localhost:${server.info.port}`);
const [res] = await Events.once(req, 'response');
let result = '';
for await (const chunk of res) {
result += chunk.toString();
}
expect(result).to.equal('ok');
});
it('uses address when present instead of host', async (flags) => {
const server = Hapi.server({ host: 'no.such.domain.hapi', address: 'localhost' });
await server.start();
flags.onCleanup = () => server.stop();
expect(server.info.host).to.equal('no.such.domain.hapi');
expect(server.info.address).to.match(/^127\.0\.0\.1|::1$/); // ::1 on node v18 with ipv6 support
});
it('uses uri when present instead of host and port', async (flags) => {
const server = Hapi.server({ host: 'no.such.domain.hapi', address: 'localhost', uri: 'http://uri.example.com:8080' });
expect(server.info.uri).to.equal('http://uri.example.com:8080');
await server.start();
flags.onCleanup = () => server.stop();
expect(server.info.host).to.equal('no.such.domain.hapi');
expect(server.info.address).to.match(/^127\.0\.0\.1|::1$/); // ::1 on node v18 with ipv6 support
expect(server.info.uri).to.equal('http://uri.example.com:8080');
});
it('throws on uri ending with /', () => {
expect(() => {
Hapi.server({ uri: 'http://uri.example.com:8080/' });
}).to.throw(/Invalid server options/);
});
it('creates a server listening on a unix domain socket', { skip: process.platform === 'win32' }, async () => {
const port = Path.join(__dirname, 'hapi-server.socket');
if (Fs.existsSync(port)) {
Fs.unlinkSync(port);
}
const server = Hapi.server({ port });
expect(server.type).to.equal('socket');
await server.start();
const absSocketPath = Path.resolve(port);
expect(server.info.port).to.equal(absSocketPath);
await server.stop();
if (Fs.existsSync(port)) {
Fs.unlinkSync(port);
}
});
it('creates a server listening on a windows named pipe', async () => {
const port = '\\\\.\\pipe\\6653e55f-26ec-4268-a4f2-882f4089315c';
const server = Hapi.server({ port });
expect(server.type).to.equal('socket');
await server.start();
expect(server.info.port).to.equal(port);
await server.stop();
});
it('creates an https server when passed tls options', () => {
const tlsOptions = {
key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0UqyXDCqWDKpoNQQK/fdr0OkG4gW6DUafxdufH9GmkX/zoKz\ng/SFLrPipzSGINKWtyMvo7mPjXqqVgE10LDI3VFV8IR6fnART+AF8CW5HMBPGt/s\nfQW4W4puvBHkBxWSW1EvbecgNEIS9hTGvHXkFzm4xJ2e9DHp2xoVAjREC73B7JbF\nhc5ZGGchKw+CFmAiNysU0DmBgQcac0eg2pWoT+YGmTeQj6sRXO67n2xy/hA1DuN6\nA4WBK3wM3O4BnTG0dNbWUEbe7yAbV5gEyq57GhJIeYxRvveVDaX90LoAqM4cUH06\n6rciON0UbDHV2LP/JaH5jzBjUyCnKLLo5snlbwIDAQABAoIBAQDJm7YC3pJJUcxb\nc8x8PlHbUkJUjxzZ5MW4Zb71yLkfRYzsxrTcyQA+g+QzA4KtPY8XrZpnkgm51M8e\n+B16AcIMiBxMC6HgCF503i16LyyJiKrrDYfGy2rTK6AOJQHO3TXWJ3eT3BAGpxuS\n12K2Cq6EvQLCy79iJm7Ks+5G6EggMZPfCVdEhffRm2Epl4T7LpIAqWiUDcDfS05n\nNNfAGxxvALPn+D+kzcSF6hpmCVrFVTf9ouhvnr+0DpIIVPwSK/REAF3Ux5SQvFuL\njPmh3bGwfRtcC5d21QNrHdoBVSN2UBLmbHUpBUcOBI8FyivAWJhRfKnhTvXMFG8L\nwaXB51IZAoGBAP/E3uz6zCyN7l2j09wmbyNOi1AKvr1WSmuBJveITouwblnRSdvc\nsYm4YYE0Vb94AG4n7JIfZLKtTN0xvnCo8tYjrdwMJyGfEfMGCQQ9MpOBXAkVVZvP\ne2k4zHNNsfvSc38UNSt7K0HkVuH5BkRBQeskcsyMeu0qK4wQwdtiCoBDAoGBANF7\nFMppYxSW4ir7Jvkh0P8bP/Z7AtaSmkX7iMmUYT+gMFB5EKqFTQjNQgSJxS/uHVDE\nSC5co8WGHnRk7YH2Pp+Ty1fHfXNWyoOOzNEWvg6CFeMHW2o+/qZd4Z5Fep6qCLaa\nFvzWWC2S5YslEaaP8DQ74aAX4o+/TECrxi0z2lllAoGAdRB6qCSyRsI/k4Rkd6Lv\nw00z3lLMsoRIU6QtXaZ5rN335Awyrfr5F3vYxPZbOOOH7uM/GDJeOJmxUJxv+cia\nPQDflpPJZU4VPRJKFjKcb38JzO6C3Gm+po5kpXGuQQA19LgfDeO2DNaiHZOJFrx3\nm1R3Zr/1k491lwokcHETNVkCgYBPLjrZl6Q/8BhlLrG4kbOx+dbfj/euq5NsyHsX\n1uI7bo1Una5TBjfsD8nYdUr3pwWltcui2pl83Ak+7bdo3G8nWnIOJ/WfVzsNJzj7\n/6CvUzR6sBk5u739nJbfgFutBZBtlSkDQPHrqA7j3Ysibl3ZIJlULjMRKrnj6Ans\npCDwkQKBgQCM7gu3p7veYwCZaxqDMz5/GGFUB1My7sK0hcT7/oH61yw3O8pOekee\nuctI1R3NOudn1cs5TAy/aypgLDYTUGQTiBRILeMiZnOrvQQB9cEf7TFgDoRNCcDs\nV/ZWiegVB/WY7H0BkCekuq5bHwjgtJTpvHGqQ9YD7RhE8RSYOhdQ/Q==\n-----END RSA PRIVATE KEY-----\n',
cert: '-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDvLNml6smHlTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE0MDEyNTIxMjIxOFoXDTE1MDEyNTIxMjIxOFowRTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANFKslwwqlgyqaDUECv33a9DpBuIFug1Gn8Xbnx/RppF/86Cs4P0hS6z4qc0hiDS\nlrcjL6O5j416qlYBNdCwyN1RVfCEen5wEU/gBfAluRzATxrf7H0FuFuKbrwR5AcV\nkltRL23nIDRCEvYUxrx15Bc5uMSdnvQx6dsaFQI0RAu9weyWxYXOWRhnISsPghZg\nIjcrFNA5gYEHGnNHoNqVqE/mBpk3kI+rEVzuu59scv4QNQ7jegOFgSt8DNzuAZ0x\ntHTW1lBG3u8gG1eYBMquexoSSHmMUb73lQ2l/dC6AKjOHFB9Ouq3IjjdFGwx1diz\n/yWh+Y8wY1Mgpyiy6ObJ5W8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAoSc6Skb4\ng1e0ZqPKXBV2qbx7hlqIyYpubCl1rDiEdVzqYYZEwmst36fJRRrVaFuAM/1DYAmT\nWMhU+yTfA+vCS4tql9b9zUhPw/IDHpBDWyR01spoZFBF/hE1MGNpCSXXsAbmCiVf\naxrIgR2DNketbDxkQx671KwF1+1JOMo9ffXp+OhuRo5NaGIxhTsZ+f/MA4y084Aj\nDI39av50sTRTWWShlN+J7PtdQVA5SZD97oYbeUeL7gI18kAJww9eUdmT0nEjcwKs\nxsQT1fyKbo7AlZBY4KSlUMuGnn0VnAsB9b+LxtXlDfnjyM8bVQx1uAfRo0DO8p/5\n3J5DTjAU55deBQ==\n-----END CERTIFICATE-----\n'
};
const server = Hapi.server({ tls: tlsOptions });
expect(server.listener instanceof Https.Server).to.equal(true);
});
it('uses a provided listener', async () => {
const listener = Http.createServer();
const server = Hapi.server({ listener });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
await server.start();
const { payload } = await Wreck.get('http://localhost:' + server.info.port + '/');
expect(payload.toString()).to.equal('ok');
await server.stop();
});
it('uses a provided listener (TLS)', async () => {
const listener = Http.createServer();
const server = Hapi.server({ listener, tls: true });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
await server.start();
expect(server.info.protocol).to.equal('https');
await server.stop();
});
it('uses a provided listener with manual listen', async () => {
const listener = Http.createServer();
const server = Hapi.server({ listener, autoListen: false });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const listen = () => {
return new Promise((resolve) => listener.listen(0, 'localhost', resolve));
};
await listen();
await server.start();
const { payload } = await Wreck.get('http://localhost:' + server.info.port + '/');
expect(payload.toString()).to.equal('ok');
await server.stop();
});
it('sets info.uri with default localhost when no hostname', () => {
const orig = Os.hostname;
Os.hostname = function () {
Os.hostname = orig;
return '';
};
const server = Hapi.server({ port: 80 });
expect(server.info.uri).to.equal('http://localhost:80');
});
it('sets info.uri without port when 0', () => {
const server = Hapi.server({ host: 'example.com' });
expect(server.info.uri).to.equal('http://example.com');
});
it('closes connection on socket timeout', async () => {
const server = Hapi.server({ routes: { timeout: { socket: 50 }, payload: { timeout: 45 } } });
server.route({
method: 'GET', path: '/', options: {
handler: async (request) => {
await Hoek.wait(70);
return 'too late';
}
}
});
await server.start();
try {
await Wreck.request('GET', 'http://localhost:' + server.info.port + '/');
}
catch (err) {
expect(err.message).to.equal('Client request error: socket hang up');
}
await server.stop();
});
it('disables node socket timeout', async () => {
const server = Hapi.server({ routes: { timeout: { socket: false } } });
server.route({ method: 'GET', path: '/', handler: () => null });
await server.start();
let timeout;
const orig = Net.Socket.prototype.setTimeout;
Net.Socket.prototype.setTimeout = function (...args) {
timeout = 'gotcha';
Net.Socket.prototype.setTimeout = orig;
return orig.apply(this, args);
};
const res = await Wreck.request('GET', 'http://localhost:' + server.info.port + '/');
await Wreck.read(res);
expect(timeout).to.equal('gotcha');
await server.stop();
});
it('throws on invalid config', () => {
expect(() => {
Hapi.server({ something: false });
}).to.throw(/Invalid server options/);
});
it('combines configuration from server and defaults (cors)', () => {
const server = Hapi.server({ routes: { cors: { origin: ['example.com'] } } });
expect(server.settings.routes.cors.origin).to.equal(['example.com']);
});
it('combines configuration from server and defaults (security)', () => {
const server = Hapi.server({ routes: { security: { hsts: 2, xss: false } } });
expect(server.settings.routes.security.hsts).to.equal(2);
expect(server.settings.routes.security.xss).to.be.false();
expect(server.settings.routes.security.xframe).to.equal('deny');
expect(server.settings.routes.security.referrer).to.equal(false);
});
describe('_debug()', () => {
it('outputs 500 on ext exception', async () => {
const server = Hapi.server();
const ext = async (request) => {
await Hoek.wait(0);
const not = null;
not.here;
};
server.ext('onPreHandler', ext);
server.route({ method: 'GET', path: '/', handler: () => null });
const log = server.events.once({ name: 'request', channels: 'error' });
const orig = console.error;
console.error = function (...args) {
console.error = orig;
expect(args[0]).to.equal('Debug:');
expect(args[1]).to.equal('internal, implementation, error');
};
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
const [, event] = await log;
expect(event.error.message).to.include(['Cannot read prop', 'null', 'here']);
});
});
describe('_createCache()', () => {
it('provisions cache using engine instance', async () => {
// Config provision
const engine = new CatboxMemory();
const server = Hapi.server({ cache: { engine, name: 'test1' } });
expect(server._core.caches.get('test1').client.connection).to.shallow.equal(engine);
// Active provision
await server.cache.provision({ engine, name: 'test2' });
expect(server._core.caches.get('test2').client.connection).to.shallow.equal(engine);
// Active provision but indirect constructor
const Provider = function (options) {
this.settings = options;
};
const ref = {};
await server.cache.provision({ provider: { constructor: Provider, options: { ref } }, name: 'test3' });
expect(server._core.caches.get('test3').client.connection.settings.ref).to.shallow.equal(ref);
});
});
describe('start()', () => {
it('starts and stops', async () => {
const server = Hapi.server();
let started = 0;
let stopped = 0;
server.events.on('start', () => {
++started;
});
server.events.on('stop', () => {
++stopped;
});
await server.start();
expect(server._core.started).to.equal(true);
await server.stop();
expect(server._core.started).to.equal(false);
expect(started).to.equal(1);
expect(stopped).to.equal(1);
});
it('initializes, starts, and stops', async () => {
const server = Hapi.server();
let started = 0;
let stopped = 0;
server.events.on('start', () => {
++started;
});
server.events.on('stop', () => {
++stopped;
});
await server.initialize();
await server.start();
expect(server._core.started).to.equal(true);
await server.stop();
expect(server._core.started).to.equal(false);
expect(started).to.equal(1);
expect(stopped).to.equal(1);
});
it('does not re-initialize the server', async () => {
const server = Hapi.server();
await server.initialize();
await server.initialize();
});
it('returns connection start error', async () => {
const server1 = Hapi.server();
await server1.start();
const port = server1.info.port;
const server2 = Hapi.server({ port });
await expect(server2.start()).to.reject(/EADDRINUSE/);
await server1.stop();
});
it('returns onPostStart error', async () => {
const server = Hapi.server();
const postStart = function (srv) {
throw new Error('boom');
};
server.ext('onPostStart', postStart);
await expect(server.start()).to.reject('boom');
await server.stop();
expect(server.info.started).to.equal(0);
});
it('errors on bad cache start', async () => {
const cache = {
engine: {
start: function () {
throw new Error('oops');
},
stop: function () { }
}
};
const server = Hapi.server({ cache });
await expect(server.start()).to.reject('oops');
});
it('fails to start server when registration incomplete', async () => {
const plugin = {
name: 'plugin',
register: Hoek.ignore
};
const server = Hapi.server();
server.register(plugin);
await expect(server.start()).to.reject('Cannot start server before plugins finished registration');
});
it('fails to initialize server when not stopped', async () => {
const plugin = function () { };
plugin.attributes = { name: 'plugin' };
const server = Hapi.server();
await server.start();
await expect(server.initialize()).to.reject('Cannot initialize server while it is in started phase');
await server.stop();
});
it('fails to start server when starting', async () => {
const plugin = function () { };
plugin.attributes = { name: 'plugin' };
const server = Hapi.server();
const starting = server.start();
await expect(server.start()).to.reject('Cannot start server while it is in initializing phase');
await starting;
await server.stop();
});
});
describe('stop()', () => {
it('stops the cache', async () => {
const server = Hapi.server();
const cache = server.cache({ segment: 'test', expiresIn: 1000 });
await server.initialize();
await cache.set('a', 'going in', 0);
const value = await cache.get('a');
expect(value).to.equal('going in');
await server.stop();
await expect(cache.get('a')).to.reject();
});
it('returns an extension error (onPreStop)', async () => {
const server = Hapi.server();
const preStop = function (srv) {
throw new Error('failed cleanup');
};
server.ext('onPreStop', preStop);
await server.start();
await expect(server.stop()).to.reject('failed cleanup');
});
it('returns an extension error (onPostStop)', async () => {
const server = Hapi.server();
const postStop = function (srv) {
throw new Error('failed cleanup');
};
server.ext('onPostStop', postStop);
await server.start();
await expect(server.stop()).to.reject('failed cleanup');
});
it('returns an extension timeout (onPreStop)', async () => {
const server = Hapi.server();
const preStop = function (srv) {
return Hoek.block();
};
server.ext('onPreStop', preStop, { timeout: 100 });
await server.start();
await expect(server.stop()).to.reject('onPreStop timed out');
});
it('errors when stopping a stopping server', async () => {
const server = Hapi.server();
const stopping = server.stop();
await expect(server.stop()).to.reject('Cannot stop server while in stopping phase');
await stopping;
});
it('errors on bad cache stop', async () => {
const cache = {
engine: {
start: function () { },
stop: function () {
throw new Error('oops');
}
}
};
const server = Hapi.server({ cache });
await server.start();
await expect(server.stop()).to.reject('oops');
});
});
describe('_init()', () => {
it('clears connections on close (HTTP)', async () => {
const server = Hapi.server();
let count = 0;
server.route({
method: 'GET',
path: '/',
handler: (request, h) => {
++count;
return h.abandon;
}
});
await server.start();
const promise = Wreck.request('GET', `http://localhost:${server.info.port}/`, { rejectUnauthorized: false });
await Hoek.wait(50);
const count1 = await internals.countConnections(server);
expect(count1).to.equal(1);
expect(server._core.sockets.size).to.equal(1);
expect(count).to.equal(1);
promise.req.destroy();
await expect(promise).to.reject();
await Hoek.wait(50);
const count2 = await internals.countConnections(server);
expect(count2).to.equal(0);
expect(server._core.sockets.size).to.equal(0);
expect(count).to.equal(1);
await server.stop();
});
it('clears connections on close (HTTPS)', async () => {
const tlsOptions = {
key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0UqyXDCqWDKpoNQQK/fdr0OkG4gW6DUafxdufH9GmkX/zoKz\ng/SFLrPipzSGINKWtyMvo7mPjXqqVgE10LDI3VFV8IR6fnART+AF8CW5HMBPGt/s\nfQW4W4puvBHkBxWSW1EvbecgNEIS9hTGvHXkFzm4xJ2e9DHp2xoVAjREC73B7JbF\nhc5ZGGchKw+CFmAiNysU0DmBgQcac0eg2pWoT+YGmTeQj6sRXO67n2xy/hA1DuN6\nA4WBK3wM3O4BnTG0dNbWUEbe7yAbV5gEyq57GhJIeYxRvveVDaX90LoAqM4cUH06\n6rciON0UbDHV2LP/JaH5jzBjUyCnKLLo5snlbwIDAQABAoIBAQDJm7YC3pJJUcxb\nc8x8PlHbUkJUjxzZ5MW4Zb71yLkfRYzsxrTcyQA+g+QzA4KtPY8XrZpnkgm51M8e\n+B16AcIMiBxMC6HgCF503i16LyyJiKrrDYfGy2rTK6AOJQHO3TXWJ3eT3BAGpxuS\n12K2Cq6EvQLCy79iJm7Ks+5G6EggMZPfCVdEhffRm2Epl4T7LpIAqWiUDcDfS05n\nNNfAGxxvALPn+D+kzcSF6hpmCVrFVTf9ouhvnr+0DpIIVPwSK/REAF3Ux5SQvFuL\njPmh3bGwfRtcC5d21QNrHdoBVSN2UBLmbHUpBUcOBI8FyivAWJhRfKnhTvXMFG8L\nwaXB51IZAoGBAP/E3uz6zCyN7l2j09wmbyNOi1AKvr1WSmuBJveITouwblnRSdvc\nsYm4YYE0Vb94AG4n7JIfZLKtTN0xvnCo8tYjrdwMJyGfEfMGCQQ9MpOBXAkVVZvP\ne2k4zHNNsfvSc38UNSt7K0HkVuH5BkRBQeskcsyMeu0qK4wQwdtiCoBDAoGBANF7\nFMppYxSW4ir7Jvkh0P8bP/Z7AtaSmkX7iMmUYT+gMFB5EKqFTQjNQgSJxS/uHVDE\nSC5co8WGHnRk7YH2Pp+Ty1fHfXNWyoOOzNEWvg6CFeMHW2o+/qZd4Z5Fep6qCLaa\nFvzWWC2S5YslEaaP8DQ74aAX4o+/TECrxi0z2lllAoGAdRB6qCSyRsI/k4Rkd6Lv\nw00z3lLMsoRIU6QtXaZ5rN335Awyrfr5F3vYxPZbOOOH7uM/GDJeOJmxUJxv+cia\nPQDflpPJZU4VPRJKFjKcb38JzO6C3Gm+po5kpXGuQQA19LgfDeO2DNaiHZOJFrx3\nm1R3Zr/1k491lwokcHETNVkCgYBPLjrZl6Q/8BhlLrG4kbOx+dbfj/euq5NsyHsX\n1uI7bo1Una5TBjfsD8nYdUr3pwWltcui2pl83Ak+7bdo3G8nWnIOJ/WfVzsNJzj7\n/6CvUzR6sBk5u739nJbfgFutBZBtlSkDQPHrqA7j3Ysibl3ZIJlULjMRKrnj6Ans\npCDwkQKBgQCM7gu3p7veYwCZaxqDMz5/GGFUB1My7sK0hcT7/oH61yw3O8pOekee\nuctI1R3NOudn1cs5TAy/aypgLDYTUGQTiBRILeMiZnOrvQQB9cEf7TFgDoRNCcDs\nV/ZWiegVB/WY7H0BkCekuq5bHwjgtJTpvHGqQ9YD7RhE8RSYOhdQ/Q==\n-----END RSA PRIVATE KEY-----\n',
cert: '-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDvLNml6smHlTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE0MDEyNTIxMjIxOFoXDTE1MDEyNTIxMjIxOFowRTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANFKslwwqlgyqaDUECv33a9DpBuIFug1Gn8Xbnx/RppF/86Cs4P0hS6z4qc0hiDS\nlrcjL6O5j416qlYBNdCwyN1RVfCEen5wEU/gBfAluRzATxrf7H0FuFuKbrwR5AcV\nkltRL23nIDRCEvYUxrx15Bc5uMSdnvQx6dsaFQI0RAu9weyWxYXOWRhnISsPghZg\nIjcrFNA5gYEHGnNHoNqVqE/mBpk3kI+rEVzuu59scv4QNQ7jegOFgSt8DNzuAZ0x\ntHTW1lBG3u8gG1eYBMquexoSSHmMUb73lQ2l/dC6AKjOHFB9Ouq3IjjdFGwx1diz\n/yWh+Y8wY1Mgpyiy6ObJ5W8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAoSc6Skb4\ng1e0ZqPKXBV2qbx7hlqIyYpubCl1rDiEdVzqYYZEwmst36fJRRrVaFuAM/1DYAmT\nWMhU+yTfA+vCS4tql9b9zUhPw/IDHpBDWyR01spoZFBF/hE1MGNpCSXXsAbmCiVf\naxrIgR2DNketbDxkQx671KwF1+1JOMo9ffXp+OhuRo5NaGIxhTsZ+f/MA4y084Aj\nDI39av50sTRTWWShlN+J7PtdQVA5SZD97oYbeUeL7gI18kAJww9eUdmT0nEjcwKs\nxsQT1fyKbo7AlZBY4KSlUMuGnn0VnAsB9b+LxtXlDfnjyM8bVQx1uAfRo0DO8p/5\n3J5DTjAU55deBQ==\n-----END CERTIFICATE-----\n'
};
const server = Hapi.server({ tls: tlsOptions });
let count = 0;
server.route({
method: 'GET',
path: '/',
handler: (request, h) => {
++count;
return h.abandon;
}
});
await server.start();
const promise = Wreck.request('GET', `https://localhost:${server.info.port}/`, { rejectUnauthorized: false });
await Hoek.wait(100);
const count1 = await internals.countConnections(server);
expect(count1).to.equal(1);
expect(server._core.sockets.size).to.equal(1);
expect(count).to.equal(1);
promise.req.destroy();
await expect(promise).to.reject();
await Hoek.wait(50);
const count2 = await internals.countConnections(server);
expect(count2).to.equal(0);
expect(server._core.sockets.size).to.equal(0);
expect(count).to.equal(1);
await server.stop();
});
});
describe('_start()', () => {
it('starts connection', async () => {
const server = Hapi.server();
await server.start();
let expectedBoundAddress = '0.0.0.0';
if (Net.isIPv6(server.listener.address().address)) {
expectedBoundAddress = '::';
}
expect(server.info.host).to.equal(Os.hostname());
expect(server.info.address).to.equal(expectedBoundAddress);
expect(server.info.port).to.be.a.number().and.above(1);
await server.stop();
});
it('starts connection (tls)', async () => {
const tlsOptions = {
key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0UqyXDCqWDKpoNQQK/fdr0OkG4gW6DUafxdufH9GmkX/zoKz\ng/SFLrPipzSGINKWtyMvo7mPjXqqVgE10LDI3VFV8IR6fnART+AF8CW5HMBPGt/s\nfQW4W4puvBHkBxWSW1EvbecgNEIS9hTGvHXkFzm4xJ2e9DHp2xoVAjREC73B7JbF\nhc5ZGGchKw+CFmAiNysU0DmBgQcac0eg2pWoT+YGmTeQj6sRXO67n2xy/hA1DuN6\nA4WBK3wM3O4BnTG0dNbWUEbe7yAbV5gEyq57GhJIeYxRvveVDaX90LoAqM4cUH06\n6rciON0UbDHV2LP/JaH5jzBjUyCnKLLo5snlbwIDAQABAoIBAQDJm7YC3pJJUcxb\nc8x8PlHbUkJUjxzZ5MW4Zb71yLkfRYzsxrTcyQA+g+QzA4KtPY8XrZpnkgm51M8e\n+B16AcIMiBxMC6HgCF503i16LyyJiKrrDYfGy2rTK6AOJQHO3TXWJ3eT3BAGpxuS\n12K2Cq6EvQLCy79iJm7Ks+5G6EggMZPfCVdEhffRm2Epl4T7LpIAqWiUDcDfS05n\nNNfAGxxvALPn+D+kzcSF6hpmCVrFVTf9ouhvnr+0DpIIVPwSK/REAF3Ux5SQvFuL\njPmh3bGwfRtcC5d21QNrHdoBVSN2UBLmbHUpBUcOBI8FyivAWJhRfKnhTvXMFG8L\nwaXB51IZAoGBAP/E3uz6zCyN7l2j09wmbyNOi1AKvr1WSmuBJveITouwblnRSdvc\nsYm4YYE0Vb94AG4n7JIfZLKtTN0xvnCo8tYjrdwMJyGfEfMGCQQ9MpOBXAkVVZvP\ne2k4zHNNsfvSc38UNSt7K0HkVuH5BkRBQeskcsyMeu0qK4wQwdtiCoBDAoGBANF7\nFMppYxSW4ir7Jvkh0P8bP/Z7AtaSmkX7iMmUYT+gMFB5EKqFTQjNQgSJxS/uHVDE\nSC5co8WGHnRk7YH2Pp+Ty1fHfXNWyoOOzNEWvg6CFeMHW2o+/qZd4Z5Fep6qCLaa\nFvzWWC2S5YslEaaP8DQ74aAX4o+/TECrxi0z2lllAoGAdRB6qCSyRsI/k4Rkd6Lv\nw00z3lLMsoRIU6QtXaZ5rN335Awyrfr5F3vYxPZbOOOH7uM/GDJeOJmxUJxv+cia\nPQDflpPJZU4VPRJKFjKcb38JzO6C3Gm+po5kpXGuQQA19LgfDeO2DNaiHZOJFrx3\nm1R3Zr/1k491lwokcHETNVkCgYBPLjrZl6Q/8BhlLrG4kbOx+dbfj/euq5NsyHsX\n1uI7bo1Una5TBjfsD8nYdUr3pwWltcui2pl83Ak+7bdo3G8nWnIOJ/WfVzsNJzj7\n/6CvUzR6sBk5u739nJbfgFutBZBtlSkDQPHrqA7j3Ysibl3ZIJlULjMRKrnj6Ans\npCDwkQKBgQCM7gu3p7veYwCZaxqDMz5/GGFUB1My7sK0hcT7/oH61yw3O8pOekee\nuctI1R3NOudn1cs5TAy/aypgLDYTUGQTiBRILeMiZnOrvQQB9cEf7TFgDoRNCcDs\nV/ZWiegVB/WY7H0BkCekuq5bHwjgtJTpvHGqQ9YD7RhE8RSYOhdQ/Q==\n-----END RSA PRIVATE KEY-----\n',
cert: '-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDvLNml6smHlTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE0MDEyNTIxMjIxOFoXDTE1MDEyNTIxMjIxOFowRTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANFKslwwqlgyqaDUECv33a9DpBuIFug1Gn8Xbnx/RppF/86Cs4P0hS6z4qc0hiDS\nlrcjL6O5j416qlYBNdCwyN1RVfCEen5wEU/gBfAluRzATxrf7H0FuFuKbrwR5AcV\nkltRL23nIDRCEvYUxrx15Bc5uMSdnvQx6dsaFQI0RAu9weyWxYXOWRhnISsPghZg\nIjcrFNA5gYEHGnNHoNqVqE/mBpk3kI+rEVzuu59scv4QNQ7jegOFgSt8DNzuAZ0x\ntHTW1lBG3u8gG1eYBMquexoSSHmMUb73lQ2l/dC6AKjOHFB9Ouq3IjjdFGwx1diz\n/yWh+Y8wY1Mgpyiy6ObJ5W8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAoSc6Skb4\ng1e0ZqPKXBV2qbx7hlqIyYpubCl1rDiEdVzqYYZEwmst36fJRRrVaFuAM/1DYAmT\nWMhU+yTfA+vCS4tql9b9zUhPw/IDHpBDWyR01spoZFBF/hE1MGNpCSXXsAbmCiVf\naxrIgR2DNketbDxkQx671KwF1+1JOMo9ffXp+OhuRo5NaGIxhTsZ+f/MA4y084Aj\nDI39av50sTRTWWShlN+J7PtdQVA5SZD97oYbeUeL7gI18kAJww9eUdmT0nEjcwKs\nxsQT1fyKbo7AlZBY4KSlUMuGnn0VnAsB9b+LxtXlDfnjyM8bVQx1uAfRo0DO8p/5\n3J5DTjAU55deBQ==\n-----END CERTIFICATE-----\n'
};
const server = Hapi.server({ host: '0.0.0.0', port: 0, tls: tlsOptions });
await server.start();
expect(server.info.host).to.equal('0.0.0.0');
expect(server.info.port).to.not.equal(0);
await server.stop();
});
it('sets info with defaults when missing hostname and address', () => {
const hostname = Os.hostname;
Os.hostname = function () {
Os.hostname = hostname;
return '';
};
const server = Hapi.server({ port: '8000' });
expect(server.info.host).to.equal('localhost');
expect(server.info.uri).to.equal('http://localhost:8000');
});
it('ignored repeated calls', async () => {
const server = Hapi.server();
await server.start();
await server.start();
await server.stop();
});
});
describe('_stop()', () => {
it('waits to stop until all connections are closed (HTTP)', async () => {
const server = Hapi.server();
await server.start();
const socket1 = await internals.socket(server);
const socket2 = await internals.socket(server);
await Hoek.wait(50);
const count1 = await internals.countConnections(server);
expect(count1).to.equal(2);
expect(server._core.sockets.size).to.equal(2);
const stop = server.stop();
socket1.end();
socket2.end();
await stop;
await Hoek.wait(10);
const count2 = await internals.countConnections(server);
expect(count2).to.equal(0);
expect(server._core.sockets.size).to.equal(0);
});
it('waits to stop until all connections are closed (HTTPS)', async () => {
const tlsOptions = {
key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0UqyXDCqWDKpoNQQK/fdr0OkG4gW6DUafxdufH9GmkX/zoKz\ng/SFLrPipzSGINKWtyMvo7mPjXqqVgE10LDI3VFV8IR6fnART+AF8CW5HMBPGt/s\nfQW4W4puvBHkBxWSW1EvbecgNEIS9hTGvHXkFzm4xJ2e9DHp2xoVAjREC73B7JbF\nhc5ZGGchKw+CFmAiNysU0DmBgQcac0eg2pWoT+YGmTeQj6sRXO67n2xy/hA1DuN6\nA4WBK3wM3O4BnTG0dNbWUEbe7yAbV5gEyq57GhJIeYxRvveVDaX90LoAqM4cUH06\n6rciON0UbDHV2LP/JaH5jzBjUyCnKLLo5snlbwIDAQABAoIBAQDJm7YC3pJJUcxb\nc8x8PlHbUkJUjxzZ5MW4Zb71yLkfRYzsxrTcyQA+g+QzA4KtPY8XrZpnkgm51M8e\n+B16AcIMiBxMC6HgCF503i16LyyJiKrrDYfGy2rTK6AOJQHO3TXWJ3eT3BAGpxuS\n12K2Cq6EvQLCy79iJm7Ks+5G6EggMZPfCVdEhffRm2Epl4T7LpIAqWiUDcDfS05n\nNNfAGxxvALPn+D+kzcSF6hpmCVrFVTf9ouhvnr+0DpIIVPwSK/REAF3Ux5SQvFuL\njPmh3bGwfRtcC5d21QNrHdoBVSN2UBLmbHUpBUcOBI8FyivAWJhRfKnhTvXMFG8L\nwaXB51IZAoGBAP/E3uz6zCyN7l2j09wmbyNOi1AKvr1WSmuBJveITouwblnRSdvc\nsYm4YYE0Vb94AG4n7JIfZLKtTN0xvnCo8tYjrdwMJyGfEfMGCQQ9MpOBXAkVVZvP\ne2k4zHNNsfvSc38UNSt7K0HkVuH5BkRBQeskcsyMeu0qK4wQwdtiCoBDAoGBANF7\nFMppYxSW4ir7Jvkh0P8bP/Z7AtaSmkX7iMmUYT+gMFB5EKqFTQjNQgSJxS/uHVDE\nSC5co8WGHnRk7YH2Pp+Ty1fHfXNWyoOOzNEWvg6CFeMHW2o+/qZd4Z5Fep6qCLaa\nFvzWWC2S5YslEaaP8DQ74aAX4o+/TECrxi0z2lllAoGAdRB6qCSyRsI/k4Rkd6Lv\nw00z3lLMsoRIU6QtXaZ5rN335Awyrfr5F3vYxPZbOOOH7uM/GDJeOJmxUJxv+cia\nPQDflpPJZU4VPRJKFjKcb38JzO6C3Gm+po5kpXGuQQA19LgfDeO2DNaiHZOJFrx3\nm1R3Zr/1k491lwokcHETNVkCgYBPLjrZl6Q/8BhlLrG4kbOx+dbfj/euq5NsyHsX\n1uI7bo1Una5TBjfsD8nYdUr3pwWltcui2pl83Ak+7bdo3G8nWnIOJ/WfVzsNJzj7\n/6CvUzR6sBk5u739nJbfgFutBZBtlSkDQPHrqA7j3Ysibl3ZIJlULjMRKrnj6Ans\npCDwkQKBgQCM7gu3p7veYwCZaxqDMz5/GGFUB1My7sK0hcT7/oH61yw3O8pOekee\nuctI1R3NOudn1cs5TAy/aypgLDYTUGQTiBRILeMiZnOrvQQB9cEf7TFgDoRNCcDs\nV/ZWiegVB/WY7H0BkCekuq5bHwjgtJTpvHGqQ9YD7RhE8RSYOhdQ/Q==\n-----END RSA PRIVATE KEY-----\n',
cert: '-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDvLNml6smHlTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE0MDEyNTIxMjIxOFoXDTE1MDEyNTIxMjIxOFowRTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANFKslwwqlgyqaDUECv33a9DpBuIFug1Gn8Xbnx/RppF/86Cs4P0hS6z4qc0hiDS\nlrcjL6O5j416qlYBNdCwyN1RVfCEen5wEU/gBfAluRzATxrf7H0FuFuKbrwR5AcV\nkltRL23nIDRCEvYUxrx15Bc5uMSdnvQx6dsaFQI0RAu9weyWxYXOWRhnISsPghZg\nIjcrFNA5gYEHGnNHoNqVqE/mBpk3kI+rEVzuu59scv4QNQ7jegOFgSt8DNzuAZ0x\ntHTW1lBG3u8gG1eYBMquexoSSHmMUb73lQ2l/dC6AKjOHFB9Ouq3IjjdFGwx1diz\n/yWh+Y8wY1Mgpyiy6ObJ5W8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAoSc6Skb4\ng1e0ZqPKXBV2qbx7hlqIyYpubCl1rDiEdVzqYYZEwmst36fJRRrVaFuAM/1DYAmT\nWMhU+yTfA+vCS4tql9b9zUhPw/IDHpBDWyR01spoZFBF/hE1MGNpCSXXsAbmCiVf\naxrIgR2DNketbDxkQx671KwF1+1JOMo9ffXp+OhuRo5NaGIxhTsZ+f/MA4y084Aj\nDI39av50sTRTWWShlN+J7PtdQVA5SZD97oYbeUeL7gI18kAJww9eUdmT0nEjcwKs\nxsQT1fyKbo7AlZBY4KSlUMuGnn0VnAsB9b+LxtXlDfnjyM8bVQx1uAfRo0DO8p/5\n3J5DTjAU55deBQ==\n-----END CERTIFICATE-----\n'
};
const server = Hapi.server({ tls: tlsOptions });
await server.start();
const socket1 = await internals.socket(server, 'tls');
const socket2 = await internals.socket(server, 'tls');
await Hoek.wait(50);
const count1 = await internals.countConnections(server);
expect(count1).to.equal(2);
expect(server._core.sockets.size).to.equal(2);
const stop = server.stop();
socket1.end();
socket2.end();
await stop;
await Hoek.wait(10);
const count2 = await internals.countConnections(server);
expect(count2).to.equal(0);
expect(server._core.sockets.size).to.equal(0);
});
it('immediately destroys unhandled connections', async () => {
const server = Hapi.server();
await server.start();
await internals.socket(server);
await internals.socket(server);
await Hoek.wait(50);
const count1 = await internals.countConnections(server);
expect(count1).to.equal(2);
const timer = new Hoek.Bench();
await server.stop({ timeout: 100 });
expect(timer.elapsed()).to.be.at.most(110);
});
it('waits to destroy handled connections until after the timeout', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request, h) => h.abandon });
await server.start();
const socket = await internals.socket(server);
socket.write('GET / HTTP/1.0\r\nHost: test\r\n\r\n');
await Hoek.wait(10);
const count1 = await internals.countConnections(server);
expect(count1).to.equal(1);
const timer = new Hoek.Bench();
await server.stop({ timeout: 20 });
expect(timer.elapsed()).to.be.at.least(19);
});
it('waits to destroy connections if they close by themselves', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request, h) => h.abandon });
await server.start();
const socket = await internals.socket(server);
socket.write('GET / HTTP/1.0\r\nHost: test\r\n\r\n');
await Hoek.wait(10);
const count1 = await internals.countConnections(server);
expect(count1).to.equal(1);
setTimeout(() => socket.end(), 100);
const timer = new Hoek.Bench();
await server.stop({ timeout: 400 });
expect(timer.elapsed()).to.be.below(300);
});
it('immediately destroys idle keep-alive connections', { retry: true }, async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => null });
await server.start();
const socket = await internals.socket(server);
socket.write('GET / HTTP/1.1\r\nHost: test\r\nConnection: Keep-Alive\r\n\r\n\r\n');
await new Promise((resolve) => socket.on('data', resolve));
const count = await internals.countConnections(server);
expect(count).to.equal(1);
const timer = new Hoek.Bench();
await server.stop({ timeout: 20 });
expect(timer.elapsed()).to.be.at.most(20);
});
it('waits to stop until connections close by themselves when cleanStop is disabled', async () => {
const server = Hapi.server({ operations: { cleanStop: false } });
server.route({ method: 'GET', path: '/', handler: (request, h) => h.abandon });
await server.start();
const socket = await internals.socket(server);
socket.write('GET / HTTP/1.0\r\nHost: test\r\n\r\n');
await Hoek.wait(10);
const count1 = await internals.countConnections(server);
expect(count1).to.equal(1);
setTimeout(() => socket.end(), 100);
const stop = server.stop();
await Hoek.wait(50);
const count2 = await internals.countConnections(server);
expect(count2).to.equal(1);
await Hoek.wait(200);
const count3 = await internals.countConnections(server);
expect(count3).to.equal(0);
await stop;
});
it('refuses to handle new incoming requests on persistent connections', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
await server.start();
const agent = new Http.Agent({ keepAlive: true, maxSockets: 1 });
const first = Wreck.get('http://localhost:' + server.info.port + '/', { agent });
const second = Wreck.get('http://localhost:' + server.info.port + '/', { agent });
const { res, payload } = await first;
const stop = server.stop();
const err = await expect(second).to.reject(Error);
await stop;
await Hoek.wait(10);
expect(res.headers.connection).to.equal('keep-alive');
expect(payload.toString()).to.equal('ok');
expect(err.code).to.equal('ECONNRESET');
expect(server._core.started).to.equal(false);
});
it('allows incoming requests during the stopping phase', async () => {
const team = new Teamwork.Team();
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
server.ext('onPreStop', () => team.work);
await server.start();
const stop = server.stop();
const { res, payload } = await Wreck.get(`http://localhost:${server.info.port}`);
team.attend(); // Allow server to finalize stop
await stop;
expect(res.headers.connection).to.equal('close');
expect(payload.toString()).to.equal('ok');
expect(server._core.started).to.equal(false);
});
it('finishes in-progress requests and ends connection', async () => {
let stop;
const handler = async (request) => {
stop = server.stop({ timeout: 200 });
await Hoek.wait(0);
return 'ok';
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
await server.start();
const agent = new Http.Agent({ keepAlive: true, maxSockets: 1 });
const first = Wreck.get('http://localhost:' + server.info.port + '/', { agent });
const second = Wreck.get('http://localhost:' + server.info.port + '/404', { agent });
const { res, payload } = await first;
expect(res.headers.connection).to.equal('close');
expect(payload.toString()).to.equal('ok');
await expect(second).to.reject();
await expect(stop).to.not.reject();
});
it('does not close longpoll HTTPS requests before response (if within timeout)', async () => {
const tlsOptions = {
key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0UqyXDCqWDKpoNQQK/fdr0OkG4gW6DUafxdufH9GmkX/zoKz\ng/SFLrPipzSGINKWtyMvo7mPjXqqVgE10LDI3VFV8IR6fnART+AF8CW5HMBPGt/s\nfQW4W4puvBHkBxWSW1EvbecgNEIS9hTGvHXkFzm4xJ2e9DHp2xoVAjREC73B7JbF\nhc5ZGGchKw+CFmAiNysU0DmBgQcac0eg2pWoT+YGmTeQj6sRXO67n2xy/hA1DuN6\nA4WBK3wM3O4BnTG0dNbWUEbe7yAbV5gEyq57GhJIeYxRvveVDaX90LoAqM4cUH06\n6rciON0UbDHV2LP/JaH5jzBjUyCnKLLo5snlbwIDAQABAoIBAQDJm7YC3pJJUcxb\nc8x8PlHbUkJUjxzZ5MW4Zb71yLkfRYzsxrTcyQA+g+QzA4KtPY8XrZpnkgm51M8e\n+B16AcIMiBxMC6HgCF503i16LyyJiKrrDYfGy2rTK6AOJQHO3TXWJ3eT3BAGpxuS\n12K2Cq6EvQLCy79iJm7Ks+5G6EggMZPfCVdEhffRm2Epl4T7LpIAqWiUDcDfS05n\nNNfAGxxvALPn+D+kzcSF6hpmCVrFVTf9ouhvnr+0DpIIVPwSK/REAF3Ux5SQvFuL\njPmh3bGwfRtcC5d21QNrHdoBVSN2UBLmbHUpBUcOBI8FyivAWJhRfKnhTvXMFG8L\nwaXB51IZAoGBAP/E3uz6zCyN7l2j09wmbyNOi1AKvr1WSmuBJveITouwblnRSdvc\nsYm4YYE0Vb94AG4n7JIfZLKtTN0xvnCo8tYjrdwMJyGfEfMGCQQ9MpOBXAkVVZvP\ne2k4zHNNsfvSc38UNSt7K0HkVuH5BkRBQeskcsyMeu0qK4wQwdtiCoBDAoGBANF7\nFMppYxSW4ir7Jvkh0P8bP/Z7AtaSmkX7iMmUYT+gMFB5EKqFTQjNQgSJxS/uHVDE\nSC5co8WGHnRk7YH2Pp+Ty1fHfXNWyoOOzNEWvg6CFeMHW2o+/qZd4Z5Fep6qCLaa\nFvzWWC2S5YslEaaP8DQ74aAX4o+/TECrxi0z2lllAoGAdRB6qCSyRsI/k4Rkd6Lv\nw00z3lLMsoRIU6QtXaZ5rN335Awyrfr5F3vYxPZbOOOH7uM/GDJeOJmxUJxv+cia\nPQDflpPJZU4VPRJKFjKcb38JzO6C3Gm+po5kpXGuQQA19LgfDeO2DNaiHZOJFrx3\nm1R3Zr/1k491lwokcHETNVkCgYBPLjrZl6Q/8BhlLrG4kbOx+dbfj/euq5NsyHsX\n1uI7bo1Una5TBjfsD8nYdUr3pwWltcui2pl83Ak+7bdo3G8nWnIOJ/WfVzsNJzj7\n/6CvUzR6sBk5u739nJbfgFutBZBtlSkDQPHrqA7j3Ysibl3ZIJlULjMRKrnj6Ans\npCDwkQKBgQCM7gu3p7veYwCZaxqDMz5/GGFUB1My7sK0hcT7/oH61yw3O8pOekee\nuctI1R3NOudn1cs5TAy/aypgLDYTUGQTiBRILeMiZnOrvQQB9cEf7TFgDoRNCcDs\nV/ZWiegVB/WY7H0BkCekuq5bHwjgtJTpvHGqQ9YD7RhE8RSYOhdQ/Q==\n-----END RSA PRIVATE KEY-----\n',
cert: '-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDvLNml6smHlTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE0MDEyNTIxMjIxOFoXDTE1MDEyNTIxMjIxOFowRTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANFKslwwqlgyqaDUECv33a9DpBuIFug1Gn8Xbnx/RppF/86Cs4P0hS6z4qc0hiDS\nlrcjL6O5j416qlYBNdCwyN1RVfCEen5wEU/gBfAluRzATxrf7H0FuFuKbrwR5AcV\nkltRL23nIDRCEvYUxrx15Bc5uMSdnvQx6dsaFQI0RAu9weyWxYXOWRhnISsPghZg\nIjcrFNA5gYEHGnNHoNqVqE/mBpk3kI+rEVzuu59scv4QNQ7jegOFgSt8DNzuAZ0x\ntHTW1lBG3u8gG1eYBMquexoSSHmMUb73lQ2l/dC6AKjOHFB9Ouq3IjjdFGwx1diz\n/yWh+Y8wY1Mgpyiy6ObJ5W8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAoSc6Skb4\ng1e0ZqPKXBV2qbx7hlqIyYpubCl1rDiEdVzqYYZEwmst36fJRRrVaFuAM/1DYAmT\nWMhU+yTfA+vCS4tql9b9zUhPw/IDHpBDWyR01spoZFBF/hE1MGNpCSXXsAbmCiVf\naxrIgR2DNketbDxkQx671KwF1+1JOMo9ffXp+OhuRo5NaGIxhTsZ+f/MA4y084Aj\nDI39av50sTRTWWShlN+J7PtdQVA5SZD97oYbeUeL7gI18kAJww9eUdmT0nEjcwKs\nxsQT1fyKbo7AlZBY4KSlUMuGnn0VnAsB9b+LxtXlDfnjyM8bVQx1uAfRo0DO8p/5\n3J5DTjAU55deBQ==\n-----END CERTIFICATE-----\n'
};
const server = Hapi.server({ tls: tlsOptions });
let stop;
const handler = async (request) => {
stop = server.stop({ timeout: 200 });
await Hoek.wait(150);
return 'ok';
};
server.route({ method: 'GET', path: '/', handler });
await server.start();
const agent = new Https.Agent({ keepAlive: true, maxSockets: 1, rejectUnauthorized: false });
const { res, payload } = await Wreck.get('https://localhost:' + server.info.port + '/', { agent });
expect(res.headers.connection).to.equal('close');
expect(payload.toString()).to.equal('ok');
await stop;
});
it('removes connection event listeners after it stops', async () => {
const server = Hapi.server();
const initial = server.listener.listeners('connection').length;
await server.start();
expect(server.listener.listeners('connection').length).to.be.greaterThan(initial);
await server.stop();
await server.start();
await server.stop();
expect(server.listener.listeners('connection').length).to.equal(initial);
});
it('ignores repeated calls', async () => {
const server = Hapi.server();
await server.stop();
await server.stop();
});
it('emits a closing event before the server\'s listener close event is emitted', async () => {
const server = Hapi.server();
const events = [];
server.events.on('closing', () => events.push('closing'));
server.events.on('stop', () => events.push('stop'));
server._core.listener.on('close', () => events.push('close'));
await server.start();
await server.stop();
expect(events).to.equal(['closing', 'close', 'stop']);
});
it('emits a closing event before the close event when there is an active request being processed', async () => {
const server = Hapi.server();
const events = [];
let stop;
const handler = async () => {
stop = server.stop({ timeout: 200 });
await Hoek.wait(0);
return 'ok';
};
server.route({ method: 'GET', path: '/', handler });
server.events.on('closing', () => events.push('closing'));
server.events.on('stop', () => events.push('stop'));
server._core.listener.on('close', () => events.push('close'));
await server.start();
const agent = new Http.Agent({ keepAlive: true, maxSockets: 1 });
// ongoing active request
const first = Wreck.get('http://localhost:' + server.info.port + '/', { agent });
// denied incoming request
const second = Wreck.get('http://localhost:' + server.info.port + '/', { agent });
const { res, payload } = await first;
expect(res.headers.connection).to.equal('close');
expect(payload.toString()).to.equal('ok');
await expect(second).to.reject();
await expect(stop).to.not.reject();
expect(events).to.equal(['closing', 'close', 'stop']);
});
});
describe('_dispatch()', () => {
it('rejects request due to high rss load', async () => {
const server = Hapi.server({ load: { sampleInterval: 5, maxRssBytes: 1 } });
let buffer;
const handler = (request) => {
buffer = buffer || Buffer.alloc(2048);
return 'ok';
};
const log = server.events.once('log');
server.route({ method: 'GET', path: '/', handler });
await server.start();
const res1 = await server.inject('/');
expect(res1.statusCode).to.equal(200);
await Hoek.wait(10);
const res2 = await server.inject('/');
expect(res2.statusCode).to.equal(503);
const [event, tags] = await log;
expect(event.channel).to.equal('internal');
expect(event.data.rss > 10000).to.equal(true);
expect(tags.load).to.be.true();
await server.stop();
});
it('doesn\'t setup listeners for cleanStop when socket is missing', async () => {
const server = Hapi.server();
server.route({
method: 'get',
path: '/',
handler: (request) => request.raw.res.listenerCount('finish')
});
const { result: normalFinishCount } = await server.inject('/');
const { _dispatch } = server._core;
server._core._dispatch = (opts) => {
const fn = _dispatch.call(server._core, opts);
return (req, res) => {
req.socket = null;
fn(req, res);
};
};
const { result: missingSocketFinishCount } = await server.inject('/');
expect(missingSocketFinishCount).to.be.lessThan(normalFinishCount);
});
});
describe('inject()', () => {
it('keeps the options.credentials object untouched', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => null });
const options = {
url: '/',
auth: {
credentials: { foo: 'bar' },
strategy: 'test'
}
};
const res = await server.inject(options);
expect(res.statusCode).to.equal(204);
expect(options.auth.credentials).to.exist();
});
it('sets credentials (with host header)', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => null });
const options = {
url: '/',
auth: {
credentials: { foo: 'bar' },
strategy: 'test'
},
headers: {
host: 'something'
}
};
const res = await server.inject(options);
expect(res.statusCode).to.equal(204);
expect(options.auth.credentials).to.exist();
});
it('sets credentials (with authority)', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.headers.host });
const options = {
url: '/',
authority: 'something',
auth: {
credentials: { foo: 'bar' },
strategy: 'test'
}
};
const res = await server.inject(options);
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('something');
expect(options.auth.credentials).to.exist();
});
it('sets authority', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.headers.host });
const options = {
url: '/',
authority: 'something'
};
const res = await server.inject(options);
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('something');
});
it('passes the options.artifacts object', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.auth.artifacts });
const options = {
url: '/',
auth: {
credentials: { foo: 'bar' },
artifacts: { bar: 'baz' },
strategy: 'test'
}
};
const res = await server.inject(options);
expect(res.statusCode).to.equal(200);
expect(res.result.bar).to.equal('baz');
expect(options.auth.artifacts).to.exist();
});
it('sets `request.auth.isInjected = true` when `auth` option is defined', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.auth.isInjected });
const options = {
url: '/',
auth: {
credentials: { foo: 'bar' },
strategy: 'test'
}
};
const res = await server.inject(options);
expect(res.statusCode).to.equal(200);
expect(res.result).to.be.true();
});
it('sets `request.isInjected = true` for requests created via `server.inject`', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.isInjected });
const options = {
url: '/'
};
const res = await server.inject(options);
expect(res.statusCode).to.equal(200);
expect(res.result).to.be.true();
});
it('`request.isInjected` access is read-only', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => {
const illegalAssignment = () => {
request.isInjected = false;
};
expect(illegalAssignment).to.throw('Cannot set property isInjected of [object Object] which has only a getter');
return request.isInjected;
} });
const options = {
url: '/'
};
const res = await server.inject(options);
expect(res.statusCode).to.equal(200);
expect(res.result).to.be.true();
});
it('sets `request.isInjected = false` for normal request', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.isInjected });
await server.start();
const { payload } = await Wreck.get(`http://localhost:${server.info.port}/`);
expect(payload.toString()).to.equal('false');
await server.stop();
});
it('sets app settings', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.app.x });
const options = {
url: '/',
authority: 'x', // For coverage
app: {
x: 123
}
};
const res = await server.inject(options);
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal(123);
});
it('sets plugins settings', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.plugins.x.y });
const options = {
url: '/',
authority: 'x', // For coverage
plugins: {
x: {
y: 123
}
}
};
const res = await server.inject(options);
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal(123);
});
it('returns the request object', async () => {
const handler = (request) => {
request.app.key = 'value';
return null;
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
expect(res.request.app.key).to.equal('value');
});
it('returns the request object for POST', async () => {
const payload = { foo: true };
const handler = (request) => {
return request.payload;
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler });
const res = await server.inject({ method: 'POST', url: '/', payload });
expect(res.statusCode).to.equal(200);
expect(JSON.parse(res.payload)).to.equal(payload);
});
it('returns the request string for POST', async () => {
const payload = JSON.stringify({ foo: true });
const handler = (request) => {
return request.payload;
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler });
const res = await server.inject({ method: 'POST', url: '/', payload });
expect(res.statusCode).to.equal(200);
expect(res.payload).to.equal(payload);
});
it('returns the request stream for POST', async () => {
const param = { foo: true };
const payload = new Stream.Readable();
payload.push(JSON.stringify(param));
payload.push(null);
const handler = (request) => {
return request.payload;
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler });
const res = await server.inject({ method: 'POST', url: '/', payload });
expect(res.statusCode).to.equal(200);
expect(JSON.parse(res.payload)).to.equal(param);
});
it('can set a client remoteAddress', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.info.remoteAddress });
const res = await server.inject({ url: '/', remoteAddress: '1.2.3.4' });
expect(res.statusCode).to.equal(200);
expect(res.payload).to.equal('1.2.3.4');
});
it('sets a default remoteAddress of 127.0.0.1', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.info.remoteAddress });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.payload).to.equal('127.0.0.1');
});
it('sets correct host header', async () => {
const server = Hapi.server({ host: 'example.com', port: 2080 });
server.route({
method: 'GET',
path: '/',
handler: (request) => request.headers.host
});
const res = await server.inject('/');
expect(res.result).to.equal('example.com:2080');
});
});
describe('table()', () => {
it('returns an array of the current routes', () => {
const server = Hapi.server();
server.route({ path: '/test/', method: 'get', handler: () => null });
server.route({ path: '/test/{p}/end', method: 'get', handler: () => null });
const routes = server.table();
expect(routes.length).to.equal(2);
expect(routes[0].path).to.equal('/test/');
});
it('combines global and vhost routes', () => {
const server = Hapi.server();
server.route({ path: '/test/', method: 'get', handler: () => null });
server.route({ path: '/test/', vhost: 'one.example.com', method: 'get', handler: () => null });
server.route({ path: '/test/', vhost: 'two.example.com', method: 'get', handler: () => null });
server.route({ path: '/test/{p}/end', method: 'get', handler: () => null });
const routes = server.table();
expect(routes.length).to.equal(4);
});
it('combines global and vhost routes and filters based on host', () => {
const server = Hapi.server();
server.route({ path: '/test/', method: 'get', handler: () => null });
server.route({ path: '/test/', vhost: 'one.example.com', method: 'get', handler: () => null });
server.route({ path: '/test/', vhost: 'two.example.com', method: 'get', handler: () => null });
server.route({ path: '/test/{p}/end', method: 'get', handler: () => null });
const routes = server.table('one.example.com');
expect(routes.length).to.equal(3);
});
it('accepts a list of hosts', () => {
const server = Hapi.server();
server.route({ path: '/test/', method: 'get', handler: () => null });
server.route({ path: '/test/', vhost: 'one.example.com', method: 'get', handler: () => null });
server.route({ path: '/test/', vhost: 'two.example.com', method: 'get', handler: () => null });
server.route({ path: '/test/{p}/end', method: 'get', handler: () => null });
const routes = server.table(['one.example.com', 'two.example.com']);
expect(routes.length).to.equal(4);
});
it('ignores unknown host', () => {
const server = Hapi.server();
server.route({ path: '/test/', method: 'get', handler: () => null });
server.route({ path: '/test/', vhost: 'one.example.com', method: 'get', handler: () => null });
server.route({ path: '/test/', vhost: 'two.example.com', method: 'get', handler: () => null });
server.route({ path: '/test/{p}/end', method: 'get', handler: () => null });
const routes = server.table('three.example.com');
expect(routes.length).to.equal(2);
});
});
describe('ext()', () => {
it('supports adding an array of methods', async () => {
const server = Hapi.server();
server.ext('onPreHandler', [
(request, h) => {
request.app.x = '1';
return h.continue;
},
(request, h) => {
request.app.x += '2';
return h.continue;
}
]);
server.route({ method: 'GET', path: '/', handler: (request) => request.app.x });
const res = await server.inject('/');
expect(res.result).to.equal('12');
});
it('sets bind via options', async () => {
const server = Hapi.server();
const preHandler = function (request, h) {
request.app.x = this.y;
return h.continue;
};
server.ext('onPreHandler', preHandler, { bind: { y: 42 } });
server.route({ method: 'GET', path: '/', handler: (request) => request.app.x });
const res = await server.inject('/');
expect(res.result).to.equal(42);
});
it('uses server views for ext added via server', async () => {
const server = Hapi.server();
await server.register(Vision);
server.views({
engines: { html: Handlebars },
path: __dirname + '/templates'
});
const preHandler = (request, h) => {
return h.view('test').takeover();
};
server.ext('onPreHandler', preHandler);
const test = {
name: 'test',
register: function (plugin, options) {
plugin.views({
engines: { html: Handlebars },
path: './no_such_directory_found'
});
plugin.route({ path: '/view', method: 'GET', handler: () => null });
}
};
await server.register(test);
const res = await server.inject('/view');
expect(res.statusCode).to.equal(200);
});
it('supports toolkit decorators on empty result', async () => {
const server = Hapi.server();
const onRequest = (request, h) => {
return h.response().redirect('/elsewhere').takeover();
};
server.ext('onRequest', onRequest);
const res = await server.inject('/');
expect(res.statusCode).to.equal(302);
expect(res.headers.location).to.equal('/elsewhere');
});
it('supports direct toolkit decorators', async () => {
const server = Hapi.server();
const onRequest = (request, h) => {
return h.redirect('/elsewhere').takeover();
};
server.ext('onRequest', onRequest);
const res = await server.inject('/');
expect(res.statusCode).to.equal(302);
expect(res.headers.location).to.equal('/elsewhere');
});
it('skips extensions once takeover is called', async () => {
const server = Hapi.server();
const preResponse1 = (request, h) => {
return h.response(1).takeover();
};
server.ext('onPreResponse', preResponse1);
let called = false;
const preResponse2 = (request) => {
called = true;
return 2;
};
server.ext('onPreResponse', preResponse2);
server.route({ method: 'GET', path: '/', handler: () => 0 });
const res = await server.inject({ method: 'GET', url: '/' });
expect(res.result).to.equal(1);
expect(called).to.be.false();
});
it('executes all extensions with return values', async () => {
const server = Hapi.server();
server.ext('onPreResponse', () => 1);
let called = false;
const preResponse2 = (request) => {
called = true;
return 2;
};
server.ext('onPreResponse', preResponse2);
server.route({ method: 'GET', path: '/', handler: () => 0 });
const res = await server.inject({ method: 'GET', url: '/' });
expect(res.result).to.equal(2);
expect(called).to.be.true();
});
describe('onRequest', () => {
it('replies with custom response', async () => {
const server = Hapi.server();
const onRequest = (request) => {
throw Boom.badRequest('boom');
};
server.ext('onRequest', onRequest);
const res = await server.inject('/');
expect(res.statusCode).to.equal(400);
expect(res.result.message).to.equal('boom');
});
it('replies with a view', async () => {
const server = Hapi.server();
await server.register(Vision);
server.views({
engines: { 'html': Handlebars },
path: __dirname + '/templates'
});
const onRequest = (request, h) => {
return h.view('test', { message: 'hola!' }).takeover();
};
server.ext('onRequest', onRequest);
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const res = await server.inject('/');
expect(res.result).to.match(/hola!<\/h1>\r?\n<\/div>\r?\n/);
});
});
describe('onPreResponse', () => {
it('replies with custom response', async () => {
const server = Hapi.server();
const preRequest = (request, h) => {
if (typeof request.response.source === 'string') {
throw Boom.badRequest('boom');
}
return h.continue;
};
server.ext('onPreResponse', preRequest);
server.route({
method: 'GET',
path: '/text',
handler: () => 'ok'
});
server.route({
method: 'GET',
path: '/obj',
handler: () => ({ status: 'ok' })
});
const res1 = await server.inject({ method: 'GET', url: '/text' });
expect(res1.result.message).to.equal('boom');
const res2 = await server.inject({ method: 'GET', url: '/obj' });
expect(res2.result.status).to.equal('ok');
});
it('intercepts 404 responses', async () => {
const server = Hapi.server();
const preResponse = (request, h) => {
return h.response(request.response.output.statusCode).takeover();
};
server.ext('onPreResponse', preResponse);
const res = await server.inject({ method: 'GET', url: '/missing' });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal(404);
});
it('intercepts 404 when using directory handler and file is missing', async () => {
const server = Hapi.server();
await server.register(Inert);
const preResponse = (request) => {
const response = request.response;
return { isBoom: response.isBoom };
};
server.ext('onPreResponse', preResponse);
server.route({ method: 'GET', path: '/{path*}', handler: { directory: { path: './somewhere', listing: false, index: true } } });
const res = await server.inject('/missing');
expect(res.statusCode).to.equal(200);
expect(res.result.isBoom).to.equal(true);
});
it('intercepts 404 when using file handler and file is missing', async () => {
const server = Hapi.server();
await server.register(Inert);
const preResponse = (request) => {
const response = request.response;
return { isBoom: response.isBoom };
};
server.ext('onPreResponse', preResponse);
server.route({ method: 'GET', path: '/{path*}', handler: { file: './somewhere/something.txt' } });
const res = await server.inject('/missing');
expect(res.statusCode).to.equal(200);
expect(res.result.isBoom).to.equal(true);
});
it('cleans unused file stream when response is overridden', { skip: !Common.hasLsof }, async () => {
const server = Hapi.server();
await server.register(Inert);
const preResponse = (request) => {
return { something: 'else' };
};
server.ext('onPreResponse', preResponse);
server.route({ method: 'GET', path: '/{path*}', handler: { directory: { path: './' } } });
const res = await server.inject('/package.json');
expect(res.statusCode).to.equal(200);
expect(res.result.something).to.equal('else');
await new Promise((resolve) => {
const cmd = ChildProcess.spawn('lsof', ['-p', process.pid]);
let lsof = '';
cmd.stdout.on('data', (buffer) => {
lsof += buffer.toString();
});
cmd.stdout.on('end', () => {
let count = 0;
const lines = lsof.split('\n');
for (let i = 0; i < lines.length; ++i) {
count += !!lines[i].match(/package.json/);
}
expect(count).to.equal(0);
resolve();
});
cmd.stdin.end();
});
});
it('executes multiple extensions', async () => {
const server = Hapi.server();
const preResponse1 = (request, h) => {
request.response.source = request.response.source + '1';
return h.continue;
};
server.ext('onPreResponse', preResponse1);
const preResponse2 = (request, h) => {
request.response.source = request.response.source + '2';
return h.continue;
};
server.ext('onPreResponse', preResponse2);
server.route({ method: 'GET', path: '/', handler: () => '0' });
const res = await server.inject({ method: 'GET', url: '/' });
expect(res.result).to.equal('012');
});
});
});
describe('route()', () => {
it('emits route event', async () => {
const server = Hapi.server();
const log = server.events.once('route');
server.route({
method: 'GET',
path: '/',
handler: () => null
});
const [route] = await log;
expect(route.path).to.equal('/');
});
it('overrides the default notFound handler', async () => {
const server = Hapi.server();
server.route({ method: '*', path: '/{p*}', handler: () => 'found' });
const res = await server.inject({ method: 'GET', url: '/page' });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('found');
});
it('responds to HEAD requests for a GET route', async () => {
const handler = (request, h) => {
return h.response('ok').etag('test').code(205);
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res1 = await server.inject({ method: 'GET', url: '/' });
expect(res1.statusCode).to.equal(205);
expect(res1.headers['content-type']).to.equal('text/html; charset=utf-8');
expect(res1.headers['content-length']).to.equal(2);
expect(res1.headers.etag).to.equal('"test"');
expect(res1.result).to.equal('ok');
const res2 = await server.inject({ method: 'HEAD', url: '/' });
expect(res2.statusCode).to.equal(res1.statusCode);
expect(res2.headers['content-type']).to.equal(res1.headers['content-type']);
expect(res2.headers['content-length']).to.equal(res1.headers['content-length']);
expect(res2.headers.etag).to.equal(res1.headers.etag);
expect(res2.result).to.not.exist();
});
it('returns 404 on HEAD requests for non-GET routes', async () => {
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler: () => 'ok' });
const res1 = await server.inject({ method: 'HEAD', url: '/' });
expect(res1.statusCode).to.equal(404);
expect(res1.result).to.not.exist();
const res2 = await server.inject({ method: 'HEAD', url: '/not-there' });
expect(res2.statusCode).to.equal(404);
expect(res2.result).to.not.exist();
});
it('returns 500 on HEAD requests for failed responses', async () => {
const preResponse = (request, h) => {
request.response._processors.marshal = function (response, callback) {
process.nextTick(callback, new Error('boom!'));
};
return h.continue;
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
server.ext('onPreResponse', preResponse);
const res1 = await server.inject({ method: 'GET', url: '/' });
expect(res1.statusCode).to.equal(500);
expect(res1.result).to.exist();
const res2 = await server.inject({ method: 'HEAD', url: '/' });
expect(res2.statusCode).to.equal(res1.statusCode);
expect(res2.headers['content-type']).to.equal(res1.headers['content-type']);
expect(res2.headers['content-length']).to.equal(res1.headers['content-length']);
expect(res2.result).to.not.exist();
});
it('allows methods array', async () => {
const server = Hapi.server();
const config = { method: ['GET', 'PUT', 'POST', 'DELETE'], path: '/', handler: (request) => request.route.method };
server.route(config);
expect(config.method).to.equal(['GET', 'PUT', 'POST', 'DELETE']); // Ensure config is cloned
const res1 = await server.inject({ method: 'HEAD', url: '/' });
expect(res1.statusCode).to.equal(200);
const res2 = await server.inject({ method: 'GET', url: '/' });
expect(res2.statusCode).to.equal(200);
expect(res2.payload).to.equal('get');
const res3 = await server.inject({ method: 'PUT', url: '/' });
expect(res3.statusCode).to.equal(200);
expect(res3.payload).to.equal('put');
const res4 = await server.inject({ method: 'POST', url: '/' });
expect(res4.statusCode).to.equal(200);
expect(res4.payload).to.equal('post');
const res5 = await server.inject({ method: 'DELETE', url: '/' });
expect(res5.statusCode).to.equal(200);
expect(res5.payload).to.equal('delete');
});
it('adds routes using single and array methods', () => {
const server = Hapi.server();
server.route([
{
method: 'GET',
path: '/api/products',
handler: () => null
},
{
method: 'GET',
path: '/api/products/{id}',
handler: () => null
},
{
method: 'POST',
path: '/api/products',
handler: () => null
},
{
method: ['PUT', 'PATCH'],
path: '/api/products/{id}',
handler: () => null
},
{
method: 'DELETE',
path: '/api/products/{id}',
handler: () => null
}
]);
const table = server.table();
const paths = table.map((route) => {
const obj = {
method: route.method,
path: route.path
};
return obj;
});
expect(table).to.have.length(6);
expect(paths).to.only.include([
{ method: 'get', path: '/api/products' },
{ method: 'get', path: '/api/products/{id}' },
{ method: 'post', path: '/api/products' },
{ method: 'put', path: '/api/products/{id}' },
{ method: 'patch', path: '/api/products/{id}' },
{ method: 'delete', path: '/api/products/{id}' }
]);
});
it('throws on methods array with id', () => {
const server = Hapi.server();
expect(() => {
server.route({
method: ['GET', 'PUT', 'POST', 'DELETE'],
path: '/',
options: {
id: 'abc',
handler: (request) => request.route.method
}
});
}).to.throw('Route id abc for path / conflicts with existing path /');
});
});
describe('_defaultRoutes()', () => {
it('returns 404 when making a request to a route that does not exist', async () => {
const server = Hapi.server();
const res = await server.inject({ method: 'GET', url: '/nope' });
expect(res.statusCode).to.equal(404);
});
it('returns 400 on bad request', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/a/{p}', handler: () => null });
const res = await server.inject('/a/%');
expect(res.statusCode).to.equal(400);
});
});
describe('load', () => {
it('measures loop delay', async () => {
const server = Hapi.server({ load: { sampleInterval: 4 } });
const handler = (request) => {
const start = Date.now();
while (Date.now() - start < 5) { }
return 'ok';
};
server.route({ method: 'GET', path: '/', handler });
await server.start();
await server.inject('/');
expect(server.load.eventLoopDelay).to.be.below(7);
await Hoek.wait(0);
await server.inject('/');
expect(server.load.eventLoopDelay).to.be.above(0);
await Hoek.wait(0);
await server.inject('/');
expect(server.load.eventLoopDelay).to.be.above(0);
expect(server.load.eventLoopUtilization).to.be.above(0);
expect(server.load.heapUsed).to.be.above(1024 * 1024);
expect(server.load.rss).to.be.above(1024 * 1024);
await server.stop();
});
});
});
internals.countConnections = function (server) {
return new Promise((resolve, reject) => {
server.listener.getConnections((err, count) => {
return (err ? reject(err) : resolve(count));
});
});
};
internals.socket = function (server, mode) {
const socket = new Net.Socket();
socket.on('error', Hoek.ignore);
if (mode === 'tls') {
socket.connect(server.info.port, '127.0.0.1');
return new Promise((resolve) => TLS.connect({ socket, rejectUnauthorized: false }, () => resolve(socket)));
}
return new Promise((resolve) => socket.connect(server.info.port, '127.0.0.1', () => resolve(socket)));
};
================================================
FILE: test/cors.js
================================================
'use strict';
const Boom = require('@hapi/boom');
const Code = require('@hapi/code');
const Hapi = require('..');
const Lab = require('@hapi/lab');
const internals = {};
const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;
describe('CORS', () => {
it('returns 404 on OPTIONS when cors disabled', async () => {
const server = Hapi.server({ routes: { cors: false } });
server.route({ method: 'GET', path: '/', handler: () => null });
const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res.statusCode).to.equal(404);
});
it('returns OPTIONS response', async () => {
const handler = function () {
throw Boom.badRequest();
};
const server = Hapi.server({ routes: { cors: true } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res.headers['access-control-allow-origin']).to.equal('http://example.com/');
});
it('returns OPTIONS response (server config)', async () => {
const handler = function () {
throw Boom.badRequest();
};
const server = Hapi.server({ routes: { cors: true } });
server.route({ method: 'GET', path: '/x', handler });
const res = await server.inject({ method: 'OPTIONS', url: '/x', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res.headers['access-control-allow-origin']).to.equal('http://example.com/');
});
it('returns headers on single route', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/a', handler: () => 'ok', options: { cors: true } });
server.route({ method: 'GET', path: '/b', handler: () => 'ok' });
const res1 = await server.inject({ method: 'OPTIONS', url: '/a', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res1.statusCode).to.equal(200);
expect(res1.result).to.be.null();
expect(res1.headers['access-control-allow-origin']).to.equal('http://example.com/');
const res2 = await server.inject({ method: 'OPTIONS', url: '/b', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res2.statusCode).to.equal(200);
expect(res2.result.message).to.equal('CORS is disabled for this route');
expect(res2.headers['access-control-allow-origin']).to.not.exist();
});
it('allows headers on multiple routes but not all', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/a', handler: () => 'ok', options: { cors: true } });
server.route({ method: 'GET', path: '/b', handler: () => 'ok', options: { cors: true } });
server.route({ method: 'GET', path: '/c', handler: () => 'ok' });
const res1 = await server.inject({ method: 'OPTIONS', url: '/a', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res1.statusCode).to.equal(200);
expect(res1.result).to.be.null();
expect(res1.headers['access-control-allow-origin']).to.equal('http://example.com/');
const res2 = await server.inject({ method: 'OPTIONS', url: '/b', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res2.statusCode).to.equal(200);
expect(res2.result).to.be.null();
expect(res2.headers['access-control-allow-origin']).to.equal('http://example.com/');
const res3 = await server.inject({ method: 'OPTIONS', url: '/c', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res3.statusCode).to.equal(200);
expect(res3.result.message).to.equal('CORS is disabled for this route');
expect(res3.headers['access-control-allow-origin']).to.not.exist();
});
it('allows same headers on multiple routes with same path', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/a', handler: () => 'ok', options: { cors: true } });
server.route({ method: 'POST', path: '/a', handler: () => 'ok', options: { cors: true } });
const res = await server.inject({ method: 'OPTIONS', url: '/a', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.be.null();
expect(res.headers['access-control-allow-origin']).to.equal('http://example.com/');
});
it('returns headers on single route (overrides defaults)', async () => {
const server = Hapi.server({ routes: { cors: { origin: ['b'] } } });
server.route({ method: 'GET', path: '/a', handler: () => 'ok', options: { cors: { origin: ['a'] } } });
server.route({ method: 'GET', path: '/b', handler: () => 'ok' });
const res1 = await server.inject({ method: 'OPTIONS', url: '/a', headers: { origin: 'a', 'access-control-request-method': 'GET' } });
expect(res1.statusCode).to.equal(200);
expect(res1.result).to.be.null();
expect(res1.headers['access-control-allow-origin']).to.equal('a');
const res2 = await server.inject({ method: 'OPTIONS', url: '/b', headers: { origin: 'b', 'access-control-request-method': 'GET' } });
expect(res2.statusCode).to.equal(200);
expect(res2.result).to.be.null();
expect(res2.headers['access-control-allow-origin']).to.equal('b');
});
it('sets access-control-allow-credentials header', async () => {
const server = Hapi.server({ routes: { cors: { credentials: true } } });
server.route({ method: 'GET', path: '/', handler: () => null });
const res = await server.inject({ url: '/', headers: { origin: 'http://example.com/' } });
expect(res.statusCode).to.equal(204);
expect(res.result).to.equal(null);
expect(res.headers['access-control-allow-credentials']).to.equal('true');
});
it('combines server defaults with route config', async () => {
const server = Hapi.server({ routes: { cors: { origin: ['http://example.com/'] } } });
server.route({ method: 'GET', path: '/', handler: () => null, options: { cors: { credentials: true } } });
const res1 = await server.inject({ url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res1.statusCode).to.equal(204);
expect(res1.result).to.equal(null);
expect(res1.headers['access-control-allow-credentials']).to.equal('true');
const res2 = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res2.statusCode).to.equal(200);
expect(res2.result).to.equal(null);
expect(res2.headers['access-control-allow-credentials']).to.equal('true');
const res3 = await server.inject({ url: '/', headers: { origin: 'http://example.org/', 'access-control-request-method': 'GET' } });
expect(res3.statusCode).to.equal(204);
expect(res3.result).to.equal(null);
expect(res3.headers['access-control-allow-credentials']).to.not.exist();
const res4 = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.org/', 'access-control-request-method': 'GET' } });
expect(res4.statusCode).to.equal(200);
expect(res4.result).to.equal({ message: 'CORS error: Origin not allowed' });
expect(res4.headers['access-control-allow-credentials']).to.not.exist();
expect(res4.headers['access-control-allow-origin']).to.not.exist();
});
it('handles request without origin header', async () => {
const server = Hapi.server({ port: 8080, routes: { cors: { origin: ['http://*.domain.com'] } } });
server.route({ method: 'GET', path: '/test', handler: () => null });
const res1 = await server.inject('/');
expect(res1.statusCode).to.equal(404);
expect(res1.headers['access-control-allow-origin']).to.not.exist();
const res2 = await server.inject('/test');
expect(res2.statusCode).to.equal(204);
expect(res2.headers['access-control-allow-origin']).to.not.exist();
});
it('handles missing routes', async () => {
const server = Hapi.server({ port: 8080, routes: { cors: { origin: ['http://*.domain.com'] } } });
const res1 = await server.inject('/');
expect(res1.statusCode).to.equal(404);
expect(res1.headers['access-control-allow-origin']).to.not.exist();
const res2 = await server.inject({ url: '/', headers: { origin: 'http://example.domain.com' } });
expect(res2.statusCode).to.equal(404);
expect(res2.headers['access-control-allow-origin']).to.exist();
});
it('uses server defaults in onRequest', async () => {
const server = Hapi.server({ port: 8080, routes: { cors: { origin: ['http://*.domain.com'] } } });
server.ext('onRequest', (request, h) => {
expect(request.info.cors).to.be.null(); // Do not set potentially incorrect information
return h.response('skip').takeover();
});
const res1 = await server.inject({ url: '/', headers: { origin: 'http://example.domain.com' } });
expect(res1.statusCode).to.equal(200);
expect(res1.headers['access-control-allow-origin']).to.exist();
const res2 = await server.inject({ url: '/', headers: { origin: 'http://example.domain.net' } });
expect(res2.statusCode).to.equal(200);
expect(res2.headers['access-control-allow-origin']).to.not.exist();
});
describe('headers()', () => {
it('returns CORS origin (route level)', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => 'ok', options: { cors: true } });
const res1 = await server.inject({ url: '/', headers: { origin: 'http://example.com/' } });
expect(res1.statusCode).to.equal(200);
expect(res1.result).to.exist();
expect(res1.result).to.equal('ok');
expect(res1.headers['access-control-allow-origin']).to.equal('http://example.com/');
const res2 = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res2.statusCode).to.equal(200);
expect(res2.result).to.be.null();
expect(res2.headers['access-control-allow-origin']).to.equal('http://example.com/');
});
it('returns CORS origin (GET)', async () => {
const server = Hapi.server({ routes: { cors: { origin: ['http://x.example.com', 'http://www.example.com'] } } });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const res = await server.inject({ url: '/', headers: { origin: 'http://x.example.com' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.exist();
expect(res.result).to.equal('ok');
expect(res.headers['access-control-allow-origin']).to.equal('http://x.example.com');
});
it('returns CORS origin (OPTIONS)', async () => {
const server = Hapi.server({ routes: { cors: { origin: ['http://test.example.com', 'http://www.example.com'] } } });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://test.example.com', 'access-control-request-method': 'GET' } });
expect(res.statusCode).to.equal(200);
expect(res.payload.length).to.equal(0);
expect(res.headers['access-control-allow-origin']).to.equal('http://test.example.com');
});
it('merges CORS access-control-expose-headers header', async () => {
const handler = (request, h) => {
return h.response('ok').header('access-control-expose-headers', 'something');
};
const server = Hapi.server({ routes: { cors: { additionalExposedHeaders: ['xyz'] } } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/', headers: { origin: 'http://example.com/' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.exist();
expect(res.result).to.equal('ok');
expect(res.headers['access-control-expose-headers']).to.equal('something,WWW-Authenticate,Server-Authorization,xyz');
});
it('returns no CORS headers when route CORS disabled', async () => {
const server = Hapi.server({ routes: { cors: { origin: ['http://test.example.com', 'http://www.example.com'] } } });
server.route({ method: 'GET', path: '/', handler: () => 'ok', options: { cors: false } });
const res = await server.inject({ url: '/', headers: { origin: 'http://x.example.com' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.exist();
expect(res.result).to.equal('ok');
expect(res.headers['access-control-allow-origin']).to.not.exist();
});
it('returns matching CORS origin', async () => {
const handler = (request, h) => {
return h.response('Tada').header('vary', 'x-test');
};
const server = Hapi.server({ compression: { minBytes: 1 }, routes: { cors: { origin: ['http://test.example.com', 'http://www.example.com', 'http://*.a.com'] } } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/', headers: { origin: 'http://www.example.com' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.exist();
expect(res.result).to.equal('Tada');
expect(res.headers['access-control-allow-origin']).to.equal('http://www.example.com');
expect(res.headers.vary).to.equal('x-test,origin,accept-encoding');
});
it('returns origin header when matching against *', async () => {
const handler = (request, h) => {
return h.response('Tada').header('vary', 'x-test');
};
const server = Hapi.server({ compression: { minBytes: 1 }, routes: { cors: { origin: ['*'] } } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/', headers: { origin: 'http://www.example.com' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.exist();
expect(res.result).to.equal('Tada');
expect(res.headers['access-control-allow-origin']).to.equal('http://www.example.com');
expect(res.headers.vary).to.equal('x-test,origin,accept-encoding');
});
it('returns * origin header when matching against * and origin is ignored', async () => {
const handler = (request, h) => {
return h.response('Tada').header('vary', 'x-test');
};
const server = Hapi.server({ compression: { minBytes: 1 }, routes: { cors: { origin: 'ignore' } } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/', headers: { origin: 'http://www.example.com' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.exist();
expect(res.result).to.equal('Tada');
expect(res.headers['access-control-allow-origin']).to.equal('*');
expect(res.headers.vary).to.equal('x-test,accept-encoding');
});
it('returns matching CORS origin wildcard', async () => {
const handler = (request, h) => {
return h.response('Tada').header('vary', 'x-test');
};
const server = Hapi.server({ compression: { minBytes: 1 }, routes: { cors: { origin: ['http://test.example.com', 'http://www.example.com', 'http://*.a.com'] } } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/', headers: { origin: 'http://www.a.com' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.exist();
expect(res.result).to.equal('Tada');
expect(res.headers['access-control-allow-origin']).to.equal('http://www.a.com');
expect(res.headers.vary).to.equal('x-test,origin,accept-encoding');
});
it('returns matching CORS origin wildcard when more than one wildcard', async () => {
const handler = (request, h) => {
return h.response('Tada').header('vary', 'x-test', true);
};
const server = Hapi.server({ compression: { minBytes: 1 }, routes: { cors: { origin: ['http://test.example.com', 'http://www.example.com', 'http://*.b.com', 'http://*.a.com'] } } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/', headers: { origin: 'http://www.a.com' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.exist();
expect(res.result).to.equal('Tada');
expect(res.headers['access-control-allow-origin']).to.equal('http://www.a.com');
expect(res.headers.vary).to.equal('x-test,origin,accept-encoding');
});
it('does not set empty CORS expose headers', async () => {
const server = Hapi.server({ routes: { cors: { exposedHeaders: [] } } });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const res1 = await server.inject({ url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res1.statusCode).to.equal(200);
expect(res1.headers['access-control-allow-origin']).to.equal('http://example.com/');
expect(res1.headers['access-control-expose-headers']).to.not.exist();
const res2 = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res2.statusCode).to.equal(200);
expect(res2.headers['access-control-allow-origin']).to.equal('http://example.com/');
expect(res2.headers['access-control-expose-headers']).to.not.exist();
});
});
describe('options()', () => {
it('ignores OPTIONS route', () => {
const server = Hapi.server();
server.route({
method: 'OPTIONS',
path: '/',
handler: () => null
});
expect(server._core.router.special.options).to.not.exist();
});
});
describe('handler()', () => {
it('errors on missing origin header', async () => {
const server = Hapi.server({ routes: { cors: true } });
server.route({
method: 'GET',
path: '/',
handler: () => null
});
const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { 'access-control-request-method': 'GET' } });
expect(res.statusCode).to.equal(404);
expect(res.result.message).to.equal('CORS error: Missing Origin header');
});
it('errors on missing access-control-request-method header', async () => {
const server = Hapi.server({ routes: { cors: true } });
server.route({
method: 'GET',
path: '/',
handler: () => null
});
const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/' } });
expect(res.statusCode).to.equal(404);
expect(res.result.message).to.equal('CORS error: Missing Access-Control-Request-Method header');
});
it('errors on missing route', async () => {
const server = Hapi.server({ routes: { cors: true } });
const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res.statusCode).to.equal(404);
});
it('errors on mismatching origin header', async () => {
const server = Hapi.server({ routes: { cors: { origin: ['a'] } } });
server.route({
method: 'GET',
path: '/',
handler: () => null
});
const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res.statusCode).to.equal(200);
expect(res.result.message).to.equal('CORS error: Origin not allowed');
});
it('matches a wildcard origin if origin is ignored and present', async () => {
const server = Hapi.server({ routes: { cors: { origin: 'ignore' } } });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const res = await server.inject({
method: 'OPTIONS',
url: '/',
headers: {
origin: 'http://test.example.com',
'access-control-request-method': 'GET',
'access-control-request-headers': 'Authorization'
}
});
expect(res.statusCode).to.equal(200);
expect(res.headers['access-control-allow-origin']).to.equal('*');
});
it('matches a wildcard origin if origin is ignored and missing', async () => {
const server = Hapi.server({ routes: { cors: { origin: 'ignore' } } });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const res = await server.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
'access-control-request-headers': 'Authorization'
}
});
expect(res.statusCode).to.equal(200);
expect(res.headers['access-control-allow-origin']).to.equal('*');
});
it('matches allowed headers', async () => {
const server = Hapi.server({ routes: { cors: true } });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const res = await server.inject({
method: 'OPTIONS',
url: '/',
headers: {
origin: 'http://test.example.com',
'access-control-request-method': 'GET',
'access-control-request-headers': 'Authorization'
}
});
expect(res.statusCode).to.equal(200);
expect(res.headers['access-control-allow-headers']).to.equal('Accept,Authorization,Content-Type,If-None-Match');
});
it('matches allowed headers (case insensitive)', async () => {
const server = Hapi.server({ routes: { cors: true } });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const res = await server.inject({
method: 'OPTIONS',
url: '/',
headers: {
origin: 'http://test.example.com',
'access-control-request-method': 'GET',
'access-control-request-headers': 'authorization'
}
});
expect(res.statusCode).to.equal(200);
expect(res.headers['access-control-allow-headers']).to.equal('Accept,Authorization,Content-Type,If-None-Match');
});
it('matches allowed headers (Origin explicit)', async () => {
const server = Hapi.server({ routes: { cors: { additionalHeaders: ['Origin'] } } });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const res = await server.inject({
method: 'OPTIONS',
url: '/',
headers: {
origin: 'http://test.example.com',
'access-control-request-method': 'GET',
'access-control-request-headers': 'Origin'
}
});
expect(res.statusCode).to.equal(200);
expect(res.headers['access-control-allow-headers']).to.equal('Accept,Authorization,Content-Type,If-None-Match,Origin');
expect(res.headers['access-control-expose-headers']).to.equal('WWW-Authenticate,Server-Authorization');
});
it('responds with configured preflight status code', async () => {
const server = Hapi.server({ routes: { cors: { preflightStatusCode: 204 } } });
server.route({ method: 'GET', path: '/204', handler: () => 'ok', options: { cors: true } });
server.route({ method: 'GET', path: '/200', handler: () => 'ok', options: { cors: { preflightStatusCode: 200 } } });
const res1 = await server.inject({
method: 'OPTIONS',
url: '/204',
headers: {
origin: 'http://test.example.com',
'access-control-request-method': 'GET'
}
});
expect(res1.statusCode).to.equal(204);
const res2 = await server.inject({
method: 'OPTIONS',
url: '/200',
headers: {
origin: 'http://test.example.com',
'access-control-request-method': 'GET'
}
});
expect(res2.statusCode).to.equal(200);
});
it('matches allowed headers (Origin implicit)', async () => {
const server = Hapi.server({ routes: { cors: true } });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const res = await server.inject({
method: 'OPTIONS',
url: '/',
headers: {
origin: 'http://test.example.com',
'access-control-request-method': 'GET',
'access-control-request-headers': 'Origin'
}
});
expect(res.statusCode).to.equal(200);
expect(res.headers['access-control-allow-headers']).to.equal('Accept,Authorization,Content-Type,If-None-Match');
});
it('errors on disallowed headers', async () => {
const server = Hapi.server({ routes: { cors: true } });
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const res = await server.inject({
method: 'OPTIONS',
url: '/',
headers: {
origin: 'http://test.example.com',
'access-control-request-method': 'GET',
'access-control-request-headers': 'X'
}
});
expect(res.statusCode).to.equal(200);
expect(res.result.message).to.equal('CORS error: Some headers are not allowed');
});
it('allows credentials', async () => {
const server = Hapi.server({ routes: { cors: { credentials: true } } });
server.route({
method: 'GET',
path: '/',
handler: () => null
});
const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
expect(res.statusCode).to.equal(200);
expect(res.headers['access-control-allow-credentials']).to.equal('true');
});
it('correctly finds route when using vhost setting', async () => {
const server = Hapi.server({ routes: { cors: true } });
server.route({
method: 'POST',
vhost: 'example.com',
path: '/',
handler: () => null
});
const res = await server.inject({ method: 'OPTIONS', url: 'http://example.com:4000/', headers: { origin: 'http://localhost', 'access-control-request-method': 'POST' } });
expect(res.statusCode).to.equal(200);
expect(res.headers['access-control-allow-methods']).to.equal('POST');
});
});
describe('headers()', () => {
it('skips CORS when missing origin header and wildcard does not ignore origin', async () => {
const server = Hapi.server({ routes: { cors: { origin: ['*'] } } });
server.route({
method: 'GET',
path: '/',
handler: () => 'ok'
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers['access-control-allow-origin']).to.not.exist();
});
it('uses CORS when missing origin header and wildcard ignores origin', async () => {
const server = Hapi.server({ routes: { cors: { origin: 'ignore' } } });
server.route({
method: 'GET',
path: '/',
handler: () => 'ok'
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers['access-control-allow-origin']).to.equal('*');
});
});
});
================================================
FILE: test/file/note.txt
================================================
Test
================================================
FILE: test/handler.js
================================================
'use strict';
const Boom = require('@hapi/boom');
const Code = require('@hapi/code');
const Hapi = require('..');
const Hoek = require('@hapi/hoek');
const Lab = require('@hapi/lab');
const internals = {};
const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;
describe('handler', () => {
describe('execute()', () => {
it('bypasses onPostHandler when handler calls takeover()', async () => {
const server = Hapi.server();
server.ext('onPostHandler', () => 'else');
server.route({ method: 'GET', path: '/', handler: (request, h) => 'something' });
server.route({ method: 'GET', path: '/takeover', handler: (request, h) => h.response('something').takeover() });
const res1 = await server.inject('/');
expect(res1.result).to.equal('else');
const res2 = await server.inject('/takeover');
expect(res2.result).to.equal('something');
});
it('returns 500 on handler exception (same tick)', async () => {
const server = Hapi.server({ debug: false });
const handler = (request) => {
const a = null;
a.b.c;
};
server.route({ method: 'GET', path: '/domain', handler });
const res = await server.inject('/domain');
expect(res.statusCode).to.equal(500);
});
it('returns 500 on handler exception (next tick await)', async () => {
const handler = async (request) => {
await Hoek.wait(0);
const not = null;
not.here;
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const log = server.events.once({ name: 'request', channels: 'error' });
const orig = console.error;
console.error = function (...args) {
console.error = orig;
expect(args[0]).to.equal('Debug:');
expect(args[1]).to.equal('internal, implementation, error');
};
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
const [, event] = await log;
expect(event.error.message).to.include(['Cannot read prop', 'null', 'here']);
});
});
describe('handler()', () => {
it('binds handler to route bind object', async () => {
const item = { x: 123 };
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
handler: function (request) {
return this.x;
},
bind: item
}
});
const res = await server.inject('/');
expect(res.result).to.equal(item.x);
});
it('binds handler to route bind object (toolkit)', async () => {
const item = { x: 123 };
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
handler: (request, h) => h.context.x,
bind: item
}
});
const res = await server.inject('/');
expect(res.result).to.equal(item.x);
});
it('returns 500 on ext method exception (same tick)', async () => {
const server = Hapi.server({ debug: false });
const onRequest = function () {
const a = null;
a.b.c;
};
server.ext('onRequest', onRequest);
server.route({ method: 'GET', path: '/domain', handler: () => 'neven gonna happen' });
const res = await server.inject('/domain');
expect(res.statusCode).to.equal(500);
});
it('returns 500 on custom function error', async () => {
const server = Hapi.server({ debug: false });
const onPreHandler = function (request, h) {
request.app.custom = () => {
throw new Error('oops');
};
return h.continue;
};
server.ext('onPreHandler', onPreHandler);
server.route({ method: 'GET', path: '/', handler: (request) => request.app.custom() });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
});
describe('prerequisitesConfig()', () => {
it('shows the complete prerequisite pipeline in the response', async () => {
const pre1 = (request, h) => {
return h.response('Hello').code(444);
};
const pre2 = (request) => {
return request.pre.m1 + request.pre.m3 + request.pre.m4;
};
const pre3 = async (request) => {
await Hoek.wait(0);
return ' ';
};
const pre4 = () => 'World';
const pre5 = (request) => {
return request.pre.m2 + (request.pre.m0 === null ? '!' : 'x');
};
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [
{
method: (request, h) => h.continue,
assign: 'm0'
},
[
{ method: pre1, assign: 'm1' },
{ method: pre3, assign: 'm3' },
{ method: pre4, assign: 'm4' }
],
{ method: pre2, assign: 'm2' },
{ method: pre5, assign: 'm5' }
],
handler: (request) => request.pre.m5
}
});
const res = await server.inject('/');
expect(res.result).to.equal('Hello World!');
});
it('allows a single prerequisite', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [
{ method: () => 'Hello', assign: 'p' }
],
handler: (request) => request.pre.p
}
});
const res = await server.inject('/');
expect(res.result).to.equal('Hello');
});
it('allows an empty prerequisite array', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [],
handler: () => 'Hello'
}
});
const res = await server.inject('/');
expect(res.result).to.equal('Hello');
});
it('takes over response', async () => {
const pre1 = () => 'Hello';
const pre2 = (request) => {
return request.pre.m1 + request.pre.m3 + request.pre.m4;
};
const pre3 = async (request, h) => {
await Hoek.wait(0);
return h.response(' ').takeover();
};
const pre4 = () => 'World';
const pre5 = (request) => {
return request.pre.m2 + '!';
};
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [
[
{ method: pre1, assign: 'm1' },
{ method: pre3, assign: 'm3' },
{ method: pre4, assign: 'm4' }
],
{ method: pre2, assign: 'm2' },
{ method: pre5, assign: 'm5' }
],
handler: (request) => request.pre.m5
}
});
const res = await server.inject('/');
expect(res.result).to.equal(' ');
});
it('returns error if prerequisite returns error', async () => {
const pre1 = () => 'Hello';
const pre2 = function () {
throw Boom.internal('boom');
};
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [
[{ method: pre1, assign: 'm1' }],
{ method: pre2, assign: 'm2' }
],
handler: (request) => request.pre.m1
}
});
const res = await server.inject('/');
expect(res.result.statusCode).to.equal(500);
});
it('passes wrapped object', async () => {
const pre = (request, h) => {
return h.response('Hello').code(444);
};
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [
{ method: pre, assign: 'p' }
],
handler: (request) => request.preResponses.p
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(444);
});
it('returns 500 if prerequisite throws', async () => {
const pre1 = () => 'Hello';
const pre2 = function () {
const a = null;
a.b.c = 0;
};
const server = Hapi.server({ debug: false });
server.route({
method: 'GET',
path: '/',
options: {
pre: [
[{ method: pre1, assign: 'm1' }],
{ method: pre2, assign: 'm2' }
],
handler: (request) => request.pre.m1
}
});
const res = await server.inject('/');
expect(res.result.statusCode).to.equal(500);
});
it('sets pre failAction to error', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [
{
method: () => {
throw Boom.forbidden();
},
failAction: 'error'
}
],
handler: () => 'ok'
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(403);
});
it('sets pre failAction to ignore', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [
{
method: () => {
throw Boom.forbidden();
},
failAction: 'ignore'
}
],
handler: () => 'ok'
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
});
it('sets pre failAction to log', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [
{
assign: 'before',
method: () => {
throw Boom.forbidden();
},
failAction: 'log'
}
],
handler: (request) => {
if (request.pre.before === request.preResponses.before &&
request.pre.before instanceof Error) {
return 'ok';
}
throw new Error();
}
}
});
let logged;
server.events.on({ name: 'request', channels: 'internal' }, (request, event, tags) => {
if (tags.pre &&
tags.error) {
logged = event;
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(logged.error.assign).to.equal('before');
});
it('sets pre failAction to method', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [
{
assign: 'value',
method: () => {
throw Boom.forbidden();
},
failAction: (request, h, err) => {
expect(err.output.statusCode).to.equal(403);
return 'failed';
}
}
],
handler: (request) => (request.pre.value + '!')
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('failed!');
});
it('sets pre failAction to method with takeover', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [
{
assign: 'value',
method: () => {
throw Boom.forbidden();
},
failAction: (request, h, err) => {
expect(err.output.statusCode).to.equal(403);
return h.response('failed').takeover();
}
}
],
handler: (request) => (request.pre.value + '!')
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('failed');
});
it('binds pre to route bind object', async () => {
const item = { x: 123 };
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [{
method: function (request) {
return this.x;
},
assign: 'x'
}],
handler: (request) => request.pre.x,
bind: item
}
});
const res = await server.inject('/');
expect(res.result).to.equal(item.x);
});
it('logs boom error instance as data if handler returns boom error', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
handler: function () {
throw Boom.forbidden();
}
}
});
const log = new Promise((resolve) => {
server.events.on({ name: 'request', channels: 'internal' }, (request, event, tags) => {
if (tags.handler &&
tags.error) {
resolve({ event, tags });
}
});
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(403);
const { event } = await log;
expect(event.error.isBoom).to.equal(true);
expect(event.error.output.statusCode).to.equal(403);
expect(event.error.message).to.equal('Forbidden');
expect(event.error.stack).to.exist();
});
});
describe('defaults()', () => {
it('returns handler without defaults', async () => {
const handler = function (route, options) {
return (request) => request.route.settings.app;
};
const server = Hapi.server();
server.decorate('handler', 'test', handler);
server.route({ method: 'get', path: '/', handler: { test: 'value' } });
const res = await server.inject('/');
expect(res.result).to.equal({});
});
it('returns handler with object defaults', async () => {
const handler = function (route, options) {
return (request) => request.route.settings.app;
};
handler.defaults = {
app: {
x: 1
}
};
const server = Hapi.server();
server.decorate('handler', 'test', handler);
server.route({ method: 'get', path: '/', handler: { test: 'value' } });
const res = await server.inject('/');
expect(res.result).to.equal({ x: 1 });
});
it('returns handler with function defaults', async () => {
const handler = function (route, options) {
return (request) => request.route.settings.app;
};
handler.defaults = function (method) {
return {
app: {
x: method
}
};
};
const server = Hapi.server();
server.decorate('handler', 'test', handler);
server.route({ method: 'get', path: '/', handler: { test: 'value' } });
const res = await server.inject('/');
expect(res.result).to.equal({ x: 'get' });
});
it('throws on handler with invalid defaults', () => {
const handler = function (route, options) {
return (request) => request.route.settings.app;
};
handler.defaults = 'invalid';
const server = Hapi.server();
expect(() => {
server.decorate('handler', 'test', handler);
}).to.throw('Handler defaults property must be an object or function');
});
});
});
================================================
FILE: test/headers.js
================================================
'use strict';
const Boom = require('@hapi/boom');
const { Engine: CatboxMemory } = require('@hapi/catbox-memory');
const Code = require('@hapi/code');
const Hapi = require('..');
const Inert = require('@hapi/inert');
const Lab = require('@hapi/lab');
const internals = {};
const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;
describe('Headers', () => {
describe('cache()', () => {
it('sets max-age value (method and route)', async () => {
const server = Hapi.server();
const method = function (id) {
return {
'id': 'fa0dbda9b1b',
'name': 'John Doe'
};
};
server.method('profile', method, { cache: { expiresIn: 120000, generateTimeout: 10 } });
const profileHandler = (request) => {
return server.methods.profile(0);
};
server.route({ method: 'GET', path: '/profile', options: { handler: profileHandler, cache: { expiresIn: 120000, privacy: 'private' } } });
await server.start();
const res = await server.inject('/profile');
expect(res.headers['cache-control']).to.equal('max-age=120, must-revalidate, private');
await server.stop();
});
it('sets max-age value (expiresAt)', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', options: { handler: () => null, cache: { expiresAt: '10:00' } } });
await server.start();
const res = await server.inject('/');
expect(res.headers['cache-control']).to.match(/^max-age=\d+, must-revalidate$/);
await server.stop();
});
it('returns no-cache on error', async () => {
const handler = () => {
throw Boom.badRequest();
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', options: { handler, cache: { expiresIn: 120000 } } });
const res = await server.inject('/');
expect(res.headers['cache-control']).to.equal('no-cache');
});
it('returns custom value on error', async () => {
const handler = () => {
throw Boom.badRequest();
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', options: { handler, cache: { otherwise: 'no-store' } } });
const res = await server.inject('/');
expect(res.headers['cache-control']).to.equal('no-store');
});
it('sets cache-control on error with status override', async () => {
const handler = () => {
throw Boom.badRequest();
};
const server = Hapi.server({ routes: { cache: { statuses: [200, 400] } } });
server.route({ method: 'GET', path: '/', options: { handler, cache: { expiresIn: 120000 } } });
const res = await server.inject('/');
expect(res.headers['cache-control']).to.equal('max-age=120, must-revalidate');
});
it('does not return max-age value when route is not cached', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/item2', options: { handler: () => ({ 'id': '55cf687663', 'name': 'Active Items' }) } });
const res = await server.inject('/item2');
expect(res.headers['cache-control']).to.not.equal('max-age=120, must-revalidate');
});
it('caches using non default cache', async () => {
const server = Hapi.server({ cache: { name: 'primary', provider: CatboxMemory } });
const defaults = server.cache({ segment: 'a', expiresIn: 2000, getDecoratedValue: true });
const primary = server.cache({ segment: 'a', expiresIn: 2000, getDecoratedValue: true, cache: 'primary' });
await server.start();
await defaults.set('b', 1);
await primary.set('b', 2);
const { value: value1 } = await defaults.get('b');
expect(value1).to.equal(1);
const { cached: cached2 } = await primary.get('b');
expect(cached2.item).to.equal(2);
await server.stop();
});
it('leaves existing cache-control header', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('text').code(400).header('cache-control', 'some value') });
const res = await server.inject('/');
expect(res.statusCode).to.equal(400);
expect(res.headers['cache-control']).to.equal('some value');
});
it('sets cache-control header from ttl without policy', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('text').ttl(10000) });
const res = await server.inject('/');
expect(res.headers['cache-control']).to.equal('max-age=10, must-revalidate');
});
it('sets cache-control header from ttl with disabled policy', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', options: { cache: false, handler: (request, h) => h.response('text').ttl(10000) } });
const res = await server.inject('/');
expect(res.headers['cache-control']).to.equal('max-age=10, must-revalidate');
});
it('leaves existing cache-control header (ttl)', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('text').ttl(1000).header('cache-control', 'none') });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers['cache-control']).to.equal('none');
});
it('includes caching header with 304', async () => {
const server = Hapi.server();
await server.register(Inert);
server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' }, options: { cache: { expiresIn: 60000 } } });
const res1 = await server.inject('/file');
const res2 = await server.inject({ url: '/file', headers: { 'if-modified-since': res1.headers['last-modified'] } });
expect(res2.statusCode).to.equal(304);
expect(res2.headers['cache-control']).to.equal('max-age=60, must-revalidate');
});
it('forbids caching on 304 if 200 is not included', async () => {
const server = Hapi.server({ routes: { cache: { statuses: [400] } } });
await server.register(Inert);
server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' }, options: { cache: { expiresIn: 60000 } } });
const res1 = await server.inject('/file');
const res2 = await server.inject({ url: '/file', headers: { 'if-modified-since': res1.headers['last-modified'] } });
expect(res2.statusCode).to.equal(304);
expect(res2.headers['cache-control']).to.equal('no-cache');
});
});
describe('security()', () => {
it('does not set security headers by default', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['strict-transport-security']).to.not.exist();
expect(res.headers['x-frame-options']).to.not.exist();
expect(res.headers['x-xss-protection']).to.not.exist();
expect(res.headers['x-download-options']).to.not.exist();
expect(res.headers['x-content-type-options']).to.not.exist();
});
it('returns default security headers when security is true', async () => {
const server = Hapi.server({ routes: { security: true } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['strict-transport-security']).to.equal('max-age=15768000');
expect(res.headers['x-frame-options']).to.equal('DENY');
expect(res.headers['x-xss-protection']).to.equal('0');
expect(res.headers['x-download-options']).to.equal('noopen');
expect(res.headers['x-content-type-options']).to.equal('nosniff');
});
it('does not set default security headers when the route sets security false', async () => {
const server = Hapi.server({ routes: { security: true } });
server.route({ method: 'GET', path: '/', handler: () => 'Test', options: { security: false } });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['strict-transport-security']).to.not.exist();
expect(res.headers['x-frame-options']).to.not.exist();
expect(res.headers['x-xss-protection']).to.not.exist();
expect(res.headers['x-download-options']).to.not.exist();
expect(res.headers['x-content-type-options']).to.not.exist();
});
it('does not return hsts header when secuirty.hsts is false', async () => {
const server = Hapi.server({ routes: { security: { hsts: false } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['strict-transport-security']).to.not.exist();
expect(res.headers['x-frame-options']).to.equal('DENY');
expect(res.headers['x-xss-protection']).to.equal('0');
expect(res.headers['x-download-options']).to.equal('noopen');
expect(res.headers['x-content-type-options']).to.equal('nosniff');
});
it('returns only default hsts header when security.hsts is true', async () => {
const server = Hapi.server({ routes: { security: { hsts: true } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['strict-transport-security']).to.equal('max-age=15768000');
});
it('returns correct hsts header when security.hsts is a number', async () => {
const server = Hapi.server({ routes: { security: { hsts: 123456789 } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['strict-transport-security']).to.equal('max-age=123456789');
});
it('returns correct hsts header when security.hsts is an object', async () => {
const server = Hapi.server({ routes: { security: { hsts: { maxAge: 123456789, includeSubDomains: true } } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['strict-transport-security']).to.equal('max-age=123456789; includeSubDomains');
});
it('returns the correct hsts header when security.hsts is an object only sepcifying maxAge', async () => {
const server = Hapi.server({ routes: { security: { hsts: { maxAge: 123456789 } } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['strict-transport-security']).to.equal('max-age=123456789');
});
it('returns correct hsts header when security.hsts is an object only specifying includeSubdomains', async () => {
const server = Hapi.server({ routes: { security: { hsts: { includeSubdomains: true } } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['strict-transport-security']).to.equal('max-age=15768000; includeSubDomains');
});
it('returns correct hsts header when security.hsts is an object only specifying includeSubDomains', async () => {
const server = Hapi.server({ routes: { security: { hsts: { includeSubDomains: true } } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['strict-transport-security']).to.equal('max-age=15768000; includeSubDomains');
});
it('returns correct hsts header when security.hsts is an object only specifying includeSubDomains and preload', async () => {
const server = Hapi.server({ routes: { security: { hsts: { includeSubDomains: true, preload: true } } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['strict-transport-security']).to.equal('max-age=15768000; includeSubDomains; preload');
});
it('does not return the xframe header whe security.xframe is false', async () => {
const server = Hapi.server({ routes: { security: { xframe: false } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['x-frame-options']).to.not.exist();
expect(res.headers['strict-transport-security']).to.equal('max-age=15768000');
expect(res.headers['x-xss-protection']).to.equal('0');
expect(res.headers['x-download-options']).to.equal('noopen');
expect(res.headers['x-content-type-options']).to.equal('nosniff');
});
it('returns only default xframe header when security.xframe is true', async () => {
const server = Hapi.server({ routes: { security: { xframe: true } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['x-frame-options']).to.equal('DENY');
});
it('returns correct xframe header when security.xframe is a string', async () => {
const server = Hapi.server({ routes: { security: { xframe: 'sameorigin' } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['x-frame-options']).to.equal('SAMEORIGIN');
});
it('returns correct xframe header when security.xframe is an object', async () => {
const server = Hapi.server({ routes: { security: { xframe: { rule: 'allow-from', source: 'http://example.com' } } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['x-frame-options']).to.equal('ALLOW-FROM http://example.com');
});
it('returns correct xframe header when security.xframe is an object', async () => {
const server = Hapi.server({ routes: { security: { xframe: { rule: 'deny' } } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['x-frame-options']).to.equal('DENY');
});
it('returns sameorigin xframe header when rule is allow-from but source is unspecified', async () => {
const server = Hapi.server({ routes: { security: { xframe: { rule: 'allow-from' } } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['x-frame-options']).to.equal('SAMEORIGIN');
});
it('does not set x-download-options if noOpen is false', async () => {
const server = Hapi.server({ routes: { security: { noOpen: false } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['x-download-options']).to.not.exist();
});
it('does not set x-content-type-options if noSniff is false', async () => {
const server = Hapi.server({ routes: { security: { noSniff: false } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['x-content-type-options']).to.not.exist();
});
it('sets the x-xss-protection header when security.xss is enabled', async () => {
const server = Hapi.server({ routes: { security: { xss: 'enabled' } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['x-xss-protection']).to.equal('1; mode=block');
expect(res.headers['strict-transport-security']).to.equal('max-age=15768000');
expect(res.headers['x-frame-options']).to.equal('DENY');
expect(res.headers['x-download-options']).to.equal('noopen');
expect(res.headers['x-content-type-options']).to.equal('nosniff');
});
it('sets the x-xss-protection header when security.xss is disabled', async () => {
const server = Hapi.server({ routes: { security: { xss: 'disabled' } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['x-xss-protection']).to.equal('0');
expect(res.headers['strict-transport-security']).to.equal('max-age=15768000');
expect(res.headers['x-frame-options']).to.equal('DENY');
expect(res.headers['x-download-options']).to.equal('noopen');
expect(res.headers['x-content-type-options']).to.equal('nosniff');
});
it('does not set the x-xss-protection header when security.xss is false', async () => {
const server = Hapi.server({ routes: { security: { xss: false } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['x-xss-protection']).to.not.exist();
expect(res.headers['strict-transport-security']).to.equal('max-age=15768000');
expect(res.headers['x-frame-options']).to.equal('DENY');
expect(res.headers['x-download-options']).to.equal('noopen');
expect(res.headers['x-content-type-options']).to.equal('nosniff');
});
it('does not return the referrer-policy header by default', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['referrer-policy']).to.not.exist();
});
it('does not return the referrer-policy header when security.referrer is false', async () => {
const server = Hapi.server({ routes: { security: { referrer: false } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['referrer-policy']).to.not.exist();
});
it('does not allow security.referrer to be true', () => {
let err;
try {
Hapi.server({ routes: { security: { referrer: true } } });
}
catch (ex) {
err = ex;
}
expect(err).to.exist();
});
it('returns correct referrer-policy header when security.referrer is a string with a valid value', async () => {
const server = Hapi.server({ routes: { security: { referrer: 'strict-origin-when-cross-origin' } } });
server.route({ method: 'GET', path: '/', handler: () => 'Test' });
const res = await server.inject({ url: '/' });
expect(res.result).to.exist();
expect(res.result).to.equal('Test');
expect(res.headers['referrer-policy']).to.equal('strict-origin-when-cross-origin');
});
});
describe('content()', () => {
it('does not modify content-type header when charset manually set', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('text').type('text/plain; charset=ISO-8859-1') });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers['content-type']).to.equal('text/plain; charset=ISO-8859-1');
});
it('does not modify content-type header when charset is unset', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('text').type('text/plain').charset() });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers['content-type']).to.equal('text/plain');
});
it('does not modify content-type header when charset is unset (default type)', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('text').charset() });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers['content-type']).to.equal('text/html');
});
it('does not set content-type by default on 204 response', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response().code(204) });
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
expect(res.headers['content-type']).to.equal(undefined);
});
});
});
================================================
FILE: test/index.js
================================================
'use strict';
const Code = require('@hapi/code');
const Hapi = require('..');
const Lab = require('@hapi/lab');
const internals = {};
const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;
describe('Server', () => {
it('supports new Server()', async () => {
const server = new Hapi.Server();
server.route({ method: 'GET', path: '/', handler: () => 'old school' });
const res = await server.inject('/');
expect(res.result).to.equal('old school');
});
});
================================================
FILE: test/methods.js
================================================
'use strict';
const Catbox = require('@hapi/catbox');
const { Engine: CatboxMemory } = require('@hapi/catbox-memory');
const Code = require('@hapi/code');
const Hapi = require('..');
const Hoek = require('@hapi/hoek');
const Lab = require('@hapi/lab');
const internals = {};
const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;
describe('Methods', () => {
it('registers a method', () => {
const add = function (a, b) {
return a + b;
};
const server = Hapi.server();
server.method('add', add);
const result = server.methods.add(1, 5);
expect(result).to.equal(6);
});
it('registers a method (object)', () => {
const add = function (a, b) {
return a + b;
};
const server = Hapi.server();
server.method({ name: 'add', method: add });
const result = server.methods.add(1, 5);
expect(result).to.equal(6);
});
it('registers a method with leading _', () => {
const _add = function (a, b) {
return a + b;
};
const server = Hapi.server();
server.method('_add', _add);
const result = server.methods._add(1, 5);
expect(result).to.equal(6);
});
it('registers a method with leading $', () => {
const $add = function (a, b) {
return a + b;
};
const server = Hapi.server();
server.method('$add', $add);
const result = server.methods.$add(1, 5);
expect(result).to.equal(6);
});
it('registers a method with _', () => {
const _add = function (a, b) {
return a + b;
};
const server = Hapi.server();
server.method('add_._that', _add);
const result = server.methods.add_._that(1, 5);
expect(result).to.equal(6);
});
it('registers a method with $', () => {
const $add = function (a, b) {
return a + b;
};
const server = Hapi.server();
server.method('add$.$that', $add);
const result = server.methods.add$.$that(1, 5);
expect(result).to.equal(6);
});
it('registers a method (promise)', async () => {
const add = function (a, b) {
return new Promise((resolve) => resolve(a + b));
};
const server = Hapi.server();
server.method('add', add);
const value = await server.methods.add(1, 5);
expect(value).to.equal(6);
});
it('registers a method with nested name', () => {
const add = function (a, b) {
return a + b;
};
const server = Hapi.server();
server.method('tools.add', add);
const result = server.methods.tools.add(1, 5);
expect(result).to.equal(6);
});
it('registers two methods with shared nested name', () => {
const add = function (a, b) {
return a + b;
};
const sub = function (a, b) {
return a - b;
};
const server = Hapi.server();
server.method('tools.add', add);
server.method('tools.sub', sub);
const result1 = server.methods.tools.add(1, 5);
expect(result1).to.equal(6);
const result2 = server.methods.tools.sub(1, 5);
expect(result2).to.equal(-4);
});
it('throws when registering a method with nested name twice', () => {
const server = Hapi.server();
server.method('tools.add', Hoek.ignore);
expect(() => {
server.method('tools.add', Hoek.ignore);
}).to.throw('Server method function name already exists: tools.add');
});
it('throws when registering a method with name nested through a function', () => {
const server = Hapi.server();
server.method('add', Hoek.ignore);
expect(() => {
server.method('add.another', Hoek.ignore);
}).to.throw('Invalid segment another in reach path add.another');
});
it('calls non cached method multiple times', () => {
let gen = 0;
const method = function (id) {
return { id, gen: gen++ };
};
const server = Hapi.server();
server.method('test', method);
const result1 = server.methods.test(1);
expect(result1.gen).to.equal(0);
const result2 = server.methods.test(1);
expect(result2.gen).to.equal(1);
});
it('caches method value', async () => {
let gen = 0;
const method = function (id) {
return { id, gen: gen++ };
};
const server = Hapi.server();
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
await server.initialize();
const result1 = await server.methods.test(1);
expect(result1.gen).to.equal(0);
const result2 = await server.methods.test(1);
expect(result2.gen).to.equal(0);
});
it('emits a cache policy event on cached methods with default cache provision', async () => {
const method = function (id) {
return { id };
};
const server = Hapi.server();
const cachePolicyEvent = server.events.once('cachePolicy');
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
const [policy, cacheName, segment] = await cachePolicyEvent;
expect(policy).to.be.instanceOf(Catbox.Policy);
expect(cacheName).to.equal(undefined);
expect(segment).to.equal('#test');
});
it('emits a cache policy event on cached methods with named cache provision', async () => {
const method = function (id) {
return { id };
};
const server = Hapi.server();
await server.cache.provision({ provider: CatboxMemory, name: 'named' });
const cachePolicyEvent = server.events.once('cachePolicy');
server.method('test', method, { cache: { cache: 'named', expiresIn: 1000, generateTimeout: 10 } });
const [policy, cacheName, segment] = await cachePolicyEvent;
expect(policy).to.be.instanceOf(Catbox.Policy);
expect(cacheName).to.equal('named');
expect(segment).to.equal('#test');
});
it('caches method value (async)', async () => {
let gen = 0;
const method = async function (id) {
await Hoek.wait(1);
return { id, gen: gen++ };
};
const server = Hapi.server();
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
await server.initialize();
const result1 = await server.methods.test(1);
expect(result1.gen).to.equal(0);
const result2 = await server.methods.test(1);
expect(result2.gen).to.equal(0);
});
it('caches method value (promise)', async () => {
let gen = 0;
const method = function (id) {
return new Promise((resolve, reject) => {
if (id === 2) {
return reject(new Error('boom'));
}
return resolve({ id, gen: gen++ });
});
};
const server = Hapi.server();
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
await server.initialize();
const result1 = await server.methods.test(1);
expect(result1.gen).to.equal(0);
const result2 = await server.methods.test(1);
expect(result2.gen).to.equal(0);
await expect(server.methods.test(2)).to.reject('boom');
});
it('caches method value (decorated)', async () => {
let gen = 0;
const method = function (id) {
return { id, gen: gen++ };
};
const server = Hapi.server();
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10, getDecoratedValue: true } });
await server.initialize();
const { value: result1 } = await server.methods.test(1);
expect(result1.gen).to.equal(0);
const { value: result2 } = await server.methods.test(1);
expect(result2.gen).to.equal(0);
});
it('reuses cached method value with custom key function', async () => {
let gen = 0;
const method = function (id) {
return { id, gen: gen++ };
};
const server = Hapi.server();
const generateKey = function (id) {
return '' + (id + 1);
};
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 }, generateKey });
await server.initialize();
const result1 = await server.methods.test(1);
expect(result1.gen).to.equal(0);
const result2 = await server.methods.test(1);
expect(result2.gen).to.equal(0);
});
it('errors when custom key function return null', async () => {
const method = function (id) {
return { id };
};
const server = Hapi.server();
const generateKey = function (id) {
return null;
};
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 }, generateKey });
await server.initialize();
await expect(server.methods.test(1)).to.reject('Invalid method key when invoking: test');
});
it('does not cache when custom key function returns a non-string', async () => {
const method = function (id) {
return { id };
};
const server = Hapi.server();
const generateKey = function (id) {
return 123;
};
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 }, generateKey });
await server.initialize();
await expect(server.methods.test(1)).to.reject('Invalid method key when invoking: test');
});
it('does not cache value when ttl is 0', async () => {
let gen = 0;
const method = function (id, flags) {
flags.ttl = 0;
return { id, gen: gen++ };
};
const server = Hapi.server();
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
await server.initialize();
const result1 = await server.methods.test(1);
expect(result1.gen).to.equal(0);
const result2 = await server.methods.test(1);
expect(result2.gen).to.equal(1);
});
it('generates new value after cache drop', async () => {
let gen = 0;
const method = function (id) {
return { id, gen: gen++ };
};
const server = Hapi.server();
server.method('dropTest', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
await server.initialize();
const result1 = await server.methods.dropTest(2);
expect(result1.gen).to.equal(0);
await server.methods.dropTest.cache.drop(2);
const result2 = await server.methods.dropTest(2);
expect(result2.gen).to.equal(1);
});
it('errors on invalid drop key', async () => {
let gen = 0;
const method = function (id) {
return { id, gen: gen++ };
};
const server = Hapi.server();
server.method('dropErrTest', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
await server.initialize();
const invalid = () => { };
await expect(server.methods.dropErrTest.cache.drop(invalid)).to.reject();
});
it('reports cache stats for each method', async () => {
const method = function (id) {
return { id };
};
const server = Hapi.server();
server.method('test', method, { cache: { generateTimeout: 10 } });
server.method('test2', method, { cache: { generateTimeout: 10 } });
await server.initialize();
server.methods.test(1);
expect(server.methods.test.cache.stats.gets).to.equal(1);
expect(server.methods.test2.cache.stats.gets).to.equal(0);
});
it('throws an error when name is not a string', () => {
expect(() => {
const server = Hapi.server();
server.method(0, () => { });
}).to.throw('name must be a string');
});
it('throws an error when name is invalid', () => {
expect(() => {
const server = Hapi.server();
server.method('0', () => { });
}).to.throw('Invalid name: 0');
expect(() => {
const server = Hapi.server();
server.method('a..', () => { });
}).to.throw('Invalid name: a..');
expect(() => {
const server = Hapi.server();
server.method('a.0', () => { });
}).to.throw('Invalid name: a.0');
expect(() => {
const server = Hapi.server();
server.method('.a', () => { });
}).to.throw('Invalid name: .a');
});
it('throws an error when method is not a function', () => {
expect(() => {
const server = Hapi.server();
server.method('user', 'function');
}).to.throw('method must be a function');
});
it('throws an error when options is not an object', () => {
expect(() => {
const server = Hapi.server();
server.method('user', () => { }, 'options');
}).to.throw(/Invalid method options \(user\)/);
});
it('throws an error when options.generateKey is not a function', () => {
expect(() => {
const server = Hapi.server();
server.method('user', () => { }, { generateKey: 'function' });
}).to.throw(/Invalid method options \(user\)/);
});
it('throws an error when options.cache is not valid', () => {
expect(() => {
const server = Hapi.server({ cache: CatboxMemory });
server.method('user', () => { }, { cache: { x: 'y', generateTimeout: 10 } });
}).to.throw(/Invalid cache policy configuration/);
});
it('throws an error when generateTimeout is not present', () => {
const server = Hapi.server();
expect(() => {
server.method('test', () => { }, { cache: {} });
}).to.throw('Method caching requires a timeout value in generateTimeout: test');
});
it('allows generateTimeout to be false', () => {
const server = Hapi.server();
expect(() => {
server.method('test', () => { }, { cache: { generateTimeout: false } });
}).to.not.throw();
});
it('returns timeout when method taking too long using the cache', async () => {
const server = Hapi.server({ cache: CatboxMemory });
let gen = 0;
const method = async function (id) {
await Hoek.wait(50);
return { id, gen: ++gen };
};
server.method('user', method, { cache: { expiresIn: 2000, generateTimeout: 30 } });
await server.initialize();
const id = Math.random();
const err = await expect(server.methods.user(id)).to.reject();
expect(err.output.statusCode).to.equal(503);
await Hoek.wait(30);
const result2 = await server.methods.user(id);
expect(result2.id).to.equal(id);
expect(result2.gen).to.equal(1);
});
it('supports empty key method', async () => {
const server = Hapi.server({ cache: CatboxMemory });
let gen = 0;
const terms = 'I agree to give my house';
const method = function () {
return { gen: gen++, terms };
};
server.method('tos', method, { cache: { expiresIn: 2000, generateTimeout: 10 } });
await server.initialize();
const result1 = await server.methods.tos();
expect(result1.terms).to.equal(terms);
expect(result1.gen).to.equal(0);
const result2 = await server.methods.tos();
expect(result2.terms).to.equal(terms);
expect(result2.gen).to.equal(0);
});
it('returns valid results when calling a method (with different keys) using the cache', async () => {
const server = Hapi.server({ cache: CatboxMemory });
let gen = 0;
const method = function (id) {
return { id, gen: ++gen };
};
server.method('user', method, { cache: { expiresIn: 2000, generateTimeout: 10 } });
await server.initialize();
const id1 = Math.random();
const result1 = await server.methods.user(id1);
expect(result1.id).to.equal(id1);
expect(result1.gen).to.equal(1);
const id2 = Math.random();
const result2 = await server.methods.user(id2);
expect(result2.id).to.equal(id2);
expect(result2.gen).to.equal(2);
});
it('errors when key generation fails', async () => {
const server = Hapi.server({ cache: CatboxMemory });
const method = function (id) {
return { id };
};
server.method([{ name: 'user', method, options: { cache: { expiresIn: 2000, generateTimeout: 10 } } }]);
await server.initialize();
const result1 = await server.methods.user(1);
expect(result1.id).to.equal(1);
const invalid = function () { };
await expect(server.methods.user(invalid)).to.reject('Invalid method key when invoking: user');
});
it('sets method bind without cache', () => {
const method = function (id) {
return { id, gen: this.gen++ };
};
const server = Hapi.server();
server.method('test', method, { bind: { gen: 7 } });
const result1 = server.methods.test(1);
expect(result1.gen).to.equal(7);
const result2 = server.methods.test(1);
expect(result2.gen).to.equal(8);
});
it('sets method bind with cache', async () => {
const method = function (id) {
return { id, gen: this.gen++ };
};
const server = Hapi.server();
server.method('test', method, { bind: { gen: 7 }, cache: { expiresIn: 1000, generateTimeout: 10 } });
await server.initialize();
const result1 = await server.methods.test(1);
expect(result1.gen).to.equal(7);
const result2 = await server.methods.test(1);
expect(result2.gen).to.equal(7);
});
it('shallow copies bind config', async () => {
const bind = { gen: 7 };
const method = function (id) {
return { id, gen: this.gen++, bound: this === bind };
};
const server = Hapi.server();
server.method('test', method, { bind, cache: { expiresIn: 1000, generateTimeout: 10 } });
await server.initialize();
const result1 = await server.methods.test(1);
expect(result1.gen).to.equal(7);
expect(result1.bound).to.equal(true);
const result2 = await server.methods.test(1);
expect(result2.gen).to.equal(7);
});
describe('_add()', () => {
it('handles sync method', () => {
const add = function (a, b) {
return a + b;
};
const server = Hapi.server();
server.method('add', add);
const result = server.methods.add(1, 5);
expect(result).to.equal(6);
});
it('handles sync method (direct error)', () => {
const add = function (a, b) {
return new Error('boom');
};
const server = Hapi.server();
server.method('add', add);
const result = server.methods.add(1, 5);
expect(result).to.be.instanceof(Error);
expect(result.message).to.equal('boom');
});
it('handles sync method (direct throw)', () => {
const add = function (a, b) {
throw new Error('boom');
};
const server = Hapi.server();
server.method('add', add);
expect(() => {
server.methods.add(1, 5);
}).to.throw('boom');
});
it('throws an error if unknown keys are present when making a server method using an object', () => {
const fn = function () { };
const server = Hapi.server();
expect(() => {
server.method({
name: 'fn',
method: fn,
cache: {}
});
}).to.throw(/^Invalid methodObject options/);
});
});
describe('generateKey()', () => {
it('handles string argument type', async () => {
const method = (id) => id;
const server = Hapi.server();
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
await server.initialize();
const value = await server.methods.test('x');
expect(value).to.equal('x');
});
it('handles multiple arguments', async () => {
const method = (a, b, c) => a + b + c;
const server = Hapi.server();
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
await server.initialize();
const value = await server.methods.test('a', 'b', 'c');
expect(value).to.equal('abc');
});
it('errors on invalid argument type', async () => {
const method = (id) => id;
const server = Hapi.server();
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
await server.initialize();
await expect(server.methods.test({})).to.reject('Invalid method key when invoking: test');
});
});
});
================================================
FILE: test/payload.js
================================================
'use strict';
const Events = require('events');
const Fs = require('fs');
const Http = require('http');
const Net = require('net');
const Path = require('path');
const Zlib = require('zlib');
const Boom = require('@hapi/boom');
const Code = require('@hapi/code');
const Hapi = require('..');
const Hoek = require('@hapi/hoek');
const Lab = require('@hapi/lab');
const Wreck = require('@hapi/wreck');
const internals = {};
const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;
describe('Payload', () => {
it('sets payload', async () => {
const payload = '{"x":"1","y":"2","z":"3"}';
const handler = (request) => {
expect(request.payload).to.exist();
expect(request.payload.z).to.equal('3');
expect(request.mime).to.equal('application/json');
return request.payload;
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/', options: { handler } });
const res = await server.inject({ method: 'POST', url: '/', payload });
expect(res.result).to.exist();
expect(res.result.x).to.equal('1');
});
it('handles request socket error', async () => {
let called = false;
const handler = function () {
called = true;
return null;
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/', options: { handler } });
const res = await server.inject({ method: 'POST', url: '/', payload: 'test', simulate: { error: true, end: false } });
expect(res.result).to.exist();
expect(res.result.statusCode).to.equal(500);
expect(called).to.be.false();
});
it('handles request socket close', async () => {
const handler = function () {
throw new Error('never called');
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/', options: { handler } });
const responded = server.ext('onPostResponse');
server.inject({ method: 'POST', url: '/', payload: 'test', simulate: { close: true, end: false } });
const request = await responded;
expect(request._isReplied).to.equal(true);
expect(request.response.output.statusCode).to.equal(500);
});
it('handles aborted request mid-lifecycle step', async (flags) => {
let req = null;
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
handler: async (request) => {
req.destroy();
await request.events.once('disconnect');
return 'ok';
}
});
// Register post handler that should not be called
let post = 0;
server.ext('onPostHandler', () => {
++post;
});
flags.onCleanup = () => server.stop();
await server.start();
req = Http.request({
hostname: 'localhost',
port: server.info.port,
method: 'get'
});
req.on('error', Hoek.ignore);
req.end();
const [request] = await server.events.once('response');
expect(request.response.isBoom).to.be.true();
expect(request.response.output.statusCode).to.equal(499);
expect(request.info.completed).to.be.above(0);
expect(request.info.responded).to.equal(0);
expect(post).to.equal(0);
});
it('handles aborted request', { retry: true }, async () => {
const server = Hapi.server();
server.route({ method: 'POST', path: '/', options: { handler: () => 'Success', payload: { parse: false } } });
const log = server.events.once('log');
await server.start();
const options = {
hostname: 'localhost',
port: server.info.port,
path: '/',
method: 'POST',
headers: {
'Content-Length': '10'
}
};
const req = Http.request(options, (res) => { });
req.on('error', Hoek.ignore);
req.write('Hello\n');
setTimeout(() => req.destroy(), 50);
const [event] = await log;
expect(event.error.message).to.equal('Parse Error');
await server.stop({ timeout: 10 });
});
it('errors when payload too big', async () => {
const payload = '{"x":"1","y":"2","z":"3"}';
const server = Hapi.server();
server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { maxBytes: 10 } } });
const res = await server.inject({ method: 'POST', url: '/', payload, headers: { 'content-length': payload.length } });
expect(res.statusCode).to.equal(413);
expect(res.result).to.exist();
expect(res.result.message).to.equal('Payload content length greater than maximum allowed: 10');
});
it('errors when payload too big (implicit length)', async () => {
const payload = '{"x":"1","y":"2","z":"3"}';
const server = Hapi.server();
server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { maxBytes: 10 } } });
const res = await server.inject({ method: 'POST', url: '/', payload });
expect(res.statusCode).to.equal(413);
expect(res.result).to.exist();
expect(res.result.message).to.equal('Payload content length greater than maximum allowed: 10');
});
it('errors when payload too big (file)', async () => {
const payload = '{"x":"1","y":"2","z":"3"}';
const server = Hapi.server();
server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { output: 'file', maxBytes: 10 } } });
const res = await server.inject({ method: 'POST', url: '/', payload, headers: { 'content-length': payload.length } });
expect(res.statusCode).to.equal(413);
expect(res.result).to.exist();
expect(res.result.message).to.equal('Payload content length greater than maximum allowed: 10');
});
it('errors when payload too big (file implicit length)', async () => {
const payload = '{"x":"1","y":"2","z":"3"}';
const server = Hapi.server();
server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { output: 'file', maxBytes: 10 } } });
const res = await server.inject({ method: 'POST', url: '/', payload });
expect(res.statusCode).to.equal(413);
expect(res.result).to.exist();
expect(res.result.message).to.equal('Payload content length greater than maximum allowed: 10');
});
it('errors when payload contains prototype poisoning', async () => {
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler: (request) => request.payload.x });
const payload = '{"x":"1","y":"2","z":"3","__proto__":{"x":"4"}}';
const res = await server.inject({ method: 'POST', url: '/', payload });
expect(res.statusCode).to.equal(400);
});
it('ignores when payload contains prototype poisoning', async () => {
const server = Hapi.server();
server.route({
method: 'POST',
path: '/',
options: {
payload: {
protoAction: 'ignore'
},
handler: (request) => request.payload.__proto__
}
});
const payload = '{"x":"1","y":"2","z":"3","__proto__":{"x":"4"}}';
const res = await server.inject({ method: 'POST', url: '/', payload });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal({ x: '4' });
});
it('sanitizes when payload contains prototype poisoning', async () => {
const server = Hapi.server();
server.route({
method: 'POST',
path: '/',
options: {
payload: {
protoAction: 'remove'
},
handler: (request) => request.payload.__proto__
}
});
const payload = '{"x":"1","y":"2","z":"3","__proto__":{"x":"4"}}';
const res = await server.inject({ method: 'POST', url: '/', payload });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal({});
});
it('returns 413 with response when payload is not consumed', async () => {
const payload = Buffer.alloc(10 * 1024 * 1024).toString();
const server = Hapi.server();
server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { maxBytes: 1024 * 1024 } } });
await server.start();
const uri = 'http://localhost:' + server.info.port;
const err = await expect(Wreck.post(uri, { payload })).to.reject();
expect(err.data.res.statusCode).to.equal(413);
expect(err.data.payload.toString()).to.equal('{"statusCode":413,"error":"Request Entity Too Large","message":"Payload content length greater than maximum allowed: 1048576"}');
await server.stop();
});
it('handles expect 100-continue', async () => {
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
await server.start();
const client = Net.connect(server.info.port);
await Events.once(client, 'connect');
client.write('POST / HTTP/1.1\r\nexpect: 100-continue\r\nhost: host\r\naccept-encoding: gzip\r\n' +
'content-type: application/json\r\ncontent-length: 14\r\nConnection: close\r\n\r\n');
const lines = [];
client.setEncoding('ascii');
for await (const chunk of client) {
if (chunk.startsWith('HTTP/1.1 100 Continue')) {
client.write('{"hello":true}');
}
else {
lines.push(...chunk.split('\r\n'));
}
}
const res = lines.shift();
const payload = lines.pop();
expect(res).to.equal('HTTP/1.1 200 OK');
expect(payload).to.equal('{"hello":true}');
await server.stop();
});
it('does not continue on errors before payload processing', async () => {
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
server.ext('onPreAuth', (request, h) => {
throw new Boom.forbidden();
});
await server.start();
const client = Net.connect(server.info.port);
await Events.once(client, 'connect');
client.write('POST / HTTP/1.1\r\nexpect: 100-continue\r\nhost: host\r\naccept-encoding: gzip\r\n' +
'content-type: application/json\r\ncontent-length: 14\r\nConnection: close\r\n\r\n');
let continued = false;
const lines = [];
client.setEncoding('ascii');
for await (const chunk of client) {
if (chunk.startsWith('HTTP/1.1 100 Continue')) {
client.write('{"hello":true}');
continued = true;
}
else {
lines.push(...chunk.split('\r\n'));
}
}
const res = lines.shift();
expect(res).to.equal('HTTP/1.1 403 Forbidden');
expect(continued).to.be.false();
await server.stop();
});
it('handles expect 100-continue on undefined routes', async () => {
const server = Hapi.server();
await server.start();
const client = Net.connect(server.info.port);
await Events.once(client, 'connect');
client.write('POST / HTTP/1.1\r\nexpect: 100-continue\r\nhost: host\r\naccept-encoding: gzip\r\n' +
'content-type: application/json\r\ncontent-length: 14\r\nConnection: close\r\n\r\n');
let continued = false;
const lines = [];
client.setEncoding('ascii');
for await (const chunk of client) {
if (chunk.startsWith('HTTP/1.1 100 Continue')) {
client.write('{"hello":true}');
continued = true;
}
else {
lines.push(...chunk.split('\r\n'));
}
}
const res = lines.shift();
expect(res).to.equal('HTTP/1.1 404 Not Found');
expect(continued).to.be.false();
await server.stop();
});
it('does not continue on custom request.payload', async () => {
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
server.ext('onRequest', (request, h) => {
request.payload = { custom: true };
return h.continue;
});
await server.start();
const client = Net.connect(server.info.port);
await Events.once(client, 'connect');
client.write('POST / HTTP/1.1\r\nexpect: 100-continue\r\nhost: host\r\naccept-encoding: gzip\r\n' +
'content-type: application/json\r\ncontent-length: 14\r\nConnection: close\r\n\r\n');
let continued = false;
const lines = [];
client.setEncoding('ascii');
for await (const chunk of client) {
if (chunk.startsWith('HTTP/1.1 100 Continue')) {
client.write('{"hello":true}');
continued = true;
}
else {
lines.push(...chunk.split('\r\n'));
}
}
const res = lines.shift();
const payload = lines.pop();
expect(res).to.equal('HTTP/1.1 200 OK');
expect(payload).to.equal('{"custom":true}');
expect(continued).to.be.false();
await server.stop();
});
it('peeks at unparsed data', async () => {
let data = null;
const ext = (request, h) => {
const chunks = [];
request.events.on('peek', (chunk, encoding) => {
chunks.push(chunk);
});
request.events.once('finish', () => {
data = Buffer.concat(chunks);
});
return h.continue;
};
const server = Hapi.server();
server.ext('onRequest', ext);
server.route({ method: 'POST', path: '/', options: { handler: () => data, payload: { parse: false } } });
const payload = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789';
const res = await server.inject({ method: 'POST', url: '/', payload });
expect(res.result).to.equal(payload);
});
it('peeks at unparsed data (finish only)', async () => {
let peeked = false;
const ext = (request, h) => {
request.events.once('finish', () => {
peeked = true;
});
return h.continue;
};
const server = Hapi.server();
server.ext('onRequest', ext);
server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { parse: false } } });
const payload = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789';
await server.inject({ method: 'POST', url: '/', payload });
expect(peeked).to.be.true();
});
it('handles gzipped payload', async () => {
const message = { 'msg': 'This message is going to be gzipped.' };
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
const compressed = await new Promise((resolve) => Zlib.gzip(JSON.stringify(message), (ignore, result) => resolve(result)));
const request = {
method: 'POST',
url: '/',
headers: {
'content-type': 'application/json',
'content-encoding': 'gzip',
'content-length': compressed.length
},
payload: compressed
};
const res = await server.inject(request);
expect(res.result).to.exist();
expect(res.result).to.equal(message);
});
it('handles deflated payload', async () => {
const message = { 'msg': 'This message is going to be gzipped.' };
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
const compressed = await new Promise((resolve) => Zlib.deflate(JSON.stringify(message), (ignore, result) => resolve(result)));
const request = {
method: 'POST',
url: '/',
headers: {
'content-type': 'application/json',
'content-encoding': 'deflate',
'content-length': compressed.length
},
payload: compressed
};
const res = await server.inject(request);
expect(res.result).to.exist();
expect(res.result).to.equal(message);
});
it('handles custom compression', async () => {
const message = { 'msg': 'This message is going to be gzipped.' };
const server = Hapi.server({ routes: { payload: { compression: { test: { some: 'options' } } } } });
const decoder = (options) => {
expect(options).to.equal({ some: 'options' });
return Zlib.createGunzip();
};
server.decoder('test', decoder);
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
const compressed = await new Promise((resolve) => Zlib.gzip(JSON.stringify(message), (ignore, result) => resolve(result)));
const request = {
method: 'POST',
url: '/',
headers: {
'content-type': 'application/json',
'content-encoding': 'test',
'content-length': compressed.length
},
payload: compressed
};
const res = await server.inject(request);
expect(res.result).to.exist();
expect(res.result).to.equal(message);
});
it('saves a file after content decoding', async () => {
const path = Path.join(__dirname, './file/image.jpg');
const sourceContents = Fs.readFileSync(path);
const stats = Fs.statSync(path);
const handler = (request) => {
const receivedContents = Fs.readFileSync(request.payload.path);
Fs.unlinkSync(request.payload.path);
expect(receivedContents).to.equal(sourceContents);
return request.payload.bytes;
};
const compressed = await new Promise((resolve) => Zlib.gzip(sourceContents, (ignore, result) => resolve(result)));
const server = Hapi.server();
server.route({ method: 'POST', path: '/file', options: { handler, payload: { output: 'file' } } });
const res = await server.inject({ method: 'POST', url: '/file', payload: compressed, headers: { 'content-encoding': 'gzip' } });
expect(res.result).to.equal(stats.size);
});
it('errors saving a file without parse', async () => {
const server = Hapi.server();
server.route({ method: 'POST', path: '/file', options: { handler: Hoek.block, payload: { output: 'file', parse: false, uploads: '/a/b/c/d/not' } } });
const res = await server.inject({ method: 'POST', url: '/file', payload: 'abcde' });
expect(res.statusCode).to.equal(500);
});
it('sets parse mode when route method is * and request is POST', async () => {
const server = Hapi.server();
server.route({ method: '*', path: '/any', handler: (request) => request.payload.key });
const res = await server.inject({ url: '/any', method: 'POST', payload: { key: '09876' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('09876');
});
it('returns an error on unsupported mime type', async () => {
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler: (request) => request.payload.key });
await server.start();
const options = {
headers: {
'Content-Type': 'application/unknown',
'Content-Length': '18'
},
payload: '{ "key": "value" }'
};
const err = await expect(Wreck.post(`http://localhost:${server.info.port}/?x=4`, options)).to.reject();
expect(err.output.statusCode).to.equal(415);
await server.stop({ timeout: 1 });
});
it('ignores unsupported mime type', async () => {
const server = Hapi.server();
server.route({ method: 'POST', path: '/', options: { handler: (request) => request.payload, payload: { failAction: 'ignore' } } });
const res = await server.inject({ method: 'POST', url: '/', payload: 'testing123', headers: { 'content-type': 'application/unknown' } });
expect(res.statusCode).to.equal(204);
expect(res.result).to.equal(null);
});
it('returns 200 on octet mime type', async () => {
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler: () => 'ok' });
const res = await server.inject({ method: 'POST', url: '/', payload: 'testing123', headers: { 'content-type': 'application/octet-stream' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('ok');
});
it('returns 200 on text mime type', async () => {
const handler = (request) => {
return request.payload + '+456';
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/text', handler });
const res = await server.inject({ method: 'POST', url: '/text', payload: 'testing123', headers: { 'content-type': 'text/plain' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('testing123+456');
});
it('returns 200 on override mime type', async () => {
const server = Hapi.server();
server.route({ method: 'POST', path: '/override', options: { handler: (request) => request.payload.key, payload: { override: 'application/json' } } });
const res = await server.inject({ method: 'POST', url: '/override', payload: '{"key":"cool"}', headers: { 'content-type': 'text/plain' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('cool');
});
it('returns 200 on text mime type when allowed', async () => {
const handler = (request) => {
return request.payload + '+456';
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/textOnly', options: { handler, payload: { allow: 'text/plain' } } });
const res = await server.inject({ method: 'POST', url: '/textOnly', payload: 'testing123', headers: { 'content-type': 'text/plain' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('testing123+456');
});
it('returns 415 on non text mime type when disallowed', async () => {
const handler = (request) => {
return request.payload + '+456';
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/textOnly', options: { handler, payload: { allow: 'text/plain' } } });
const res = await server.inject({ method: 'POST', url: '/textOnly', payload: 'testing123', headers: { 'content-type': 'application/octet-stream' } });
expect(res.statusCode).to.equal(415);
});
it('returns 200 on text mime type when allowed (array)', async () => {
const handler = (request) => {
return request.payload + '+456';
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/textOnlyArray', options: { handler, payload: { allow: ['text/plain'] } } });
const res = await server.inject({ method: 'POST', url: '/textOnlyArray', payload: 'testing123', headers: { 'content-type': 'text/plain' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('testing123+456');
});
it('returns 415 on non text mime type when disallowed (array)', async () => {
const handler = (request) => {
return request.payload + '+456';
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/textOnlyArray', options: { handler, payload: { allow: ['text/plain'] } } });
const res = await server.inject({ method: 'POST', url: '/textOnlyArray', payload: 'testing123', headers: { 'content-type': 'application/octet-stream' } });
expect(res.statusCode).to.equal(415);
});
it('returns parsed multipart data (route)', async () => {
const multipartPayload =
'--AaB03x\r\n' +
'content-disposition: form-data; name="x"\r\n' +
'\r\n' +
'First\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="x"\r\n' +
'\r\n' +
'Second\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="x"\r\n' +
'\r\n' +
'Third\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="field1"\r\n' +
'\r\n' +
'Joe Blow\r\nalmost tricked you!\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="field1"\r\n' +
'\r\n' +
'Repeated name segment\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n' +
'Content-Type: text/plain\r\n' +
'\r\n' +
'... contents of file1.txt ...\r\r\n' +
'--AaB03x--\r\n';
const handler = (request) => {
const result = {};
const keys = Object.keys(request.payload);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
const value = request.payload[key];
result[key] = value._readableState ? true : value;
}
return result;
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/echo', handler, options: { payload: { multipart: true } } });
const res = await server.inject({ method: 'POST', url: '/echo', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data; boundary=AaB03x' } });
expect(Object.keys(res.result).length).to.equal(3);
expect(res.result.field1).to.exist();
expect(res.result.field1.length).to.equal(2);
expect(res.result.field1[1]).to.equal('Repeated name segment');
expect(res.result.pics).to.exist();
});
it('returns parsed multipart data (server)', async () => {
const multipartPayload =
'--AaB03x\r\n' +
'content-disposition: form-data; name="x"\r\n' +
'\r\n' +
'First\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="x"\r\n' +
'\r\n' +
'Second\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="x"\r\n' +
'\r\n' +
'Third\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="field1"\r\n' +
'\r\n' +
'Joe Blow\r\nalmost tricked you!\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="field1"\r\n' +
'\r\n' +
'Repeated name segment\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n' +
'Content-Type: text/plain\r\n' +
'\r\n' +
'... contents of file1.txt ...\r\r\n' +
'--AaB03x--\r\n';
const handler = (request) => {
const result = {};
const keys = Object.keys(request.payload);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
const value = request.payload[key];
result[key] = value._readableState ? true : value;
}
return result;
};
const server = Hapi.server({ routes: { payload: { multipart: true } } });
server.route({ method: 'POST', path: '/echo', handler });
const res = await server.inject({ method: 'POST', url: '/echo', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data; boundary=AaB03x' } });
expect(Object.keys(res.result).length).to.equal(3);
expect(res.result.field1).to.exist();
expect(res.result.field1.length).to.equal(2);
expect(res.result.field1[1]).to.equal('Repeated name segment');
expect(res.result.pics).to.exist();
});
it('places default limit on max parts in multipart payloads', async () => {
const part = '--AaB03x\r\n' + 'content-disposition: form-data; name="x"\r\n\r\n' + 'x\r\n';
const multipartPayload = part.repeat(1001) + '--AaB03x--\r\n';
const server = Hapi.server({ routes: { payload: { multipart: true } } });
server.route({ method: 'POST', path: '/', handler: () => null });
const res = await server.inject({ method: 'POST', url: '/', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data; boundary=AaB03x' } });
expect(res.statusCode).to.equal(400);
expect(res.result.message).to.equal('Invalid multipart payload format');
});
it('signals connection close when payload is unconsumed', async () => {
const payload = Buffer.alloc(1024);
const server = Hapi.server();
server.route({ method: 'POST', path: '/', options: { handler: () => 'ok', payload: { maxBytes: 1024, output: 'stream', parse: false } } });
const res = await server.inject({ method: 'POST', url: '/', payload, headers: { 'content-type': 'application/octet-stream' } });
expect(res.statusCode).to.equal(200);
expect(res.headers).to.include({ connection: 'close' });
expect(res.result).to.equal('ok');
});
it('times out when client request taking too long', async () => {
const server = Hapi.server({ routes: { payload: { timeout: 50 } } });
server.route({ method: 'POST', path: '/', handler: () => null });
await server.start();
const request = () => {
const options = {
hostname: '127.0.0.1',
port: server.info.port,
path: '/',
method: 'POST'
};
const req = Http.request(options);
req.on('error', Hoek.ignore);
req.write('{}\n');
setTimeout(() => req.end(), 100);
return new Promise((resolve) => req.once('response', resolve));
};
const timer = new Hoek.Bench();
const res = await request();
expect(res.statusCode).to.equal(408);
expect(timer.elapsed()).to.be.at.least(50);
await server.stop({ timeout: 1 });
});
it('times out when client request taking too long (route override)', async () => {
const server = Hapi.server({ routes: { payload: { timeout: false } } });
server.route({ method: 'POST', path: '/', options: { payload: { timeout: 50 }, handler: () => null } });
await server.start();
const request = () => {
const options = {
hostname: '127.0.0.1',
port: server.info.port,
path: '/',
method: 'POST'
};
const req = Http.request(options);
req.on('error', Hoek.ignore);
req.write('{}\n');
setTimeout(() => req.end(), 100);
return new Promise((resolve) => req.once('response', resolve));
};
const timer = new Hoek.Bench();
const res = await request();
expect(res.statusCode).to.equal(408);
expect(timer.elapsed()).to.be.at.least(50);
await server.stop({ timeout: 1 });
});
it('returns payload when timeout is not triggered', async () => {
const server = Hapi.server({ routes: { payload: { timeout: 50 } } });
server.route({ method: 'POST', path: '/', handler: () => 'fast' });
await server.start();
const { res } = await Wreck.post(`http://localhost:${server.info.port}/`);
expect(res.statusCode).to.equal(200);
await server.stop({ timeout: 1 });
});
it('errors if multipart payload exceeds byte limit', async () => {
const multipartPayload =
'--AaB03x\r\n' +
'content-disposition: form-data; name="x"\r\n' +
'\r\n' +
'First\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="x"\r\n' +
'\r\n' +
'Second\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="x"\r\n' +
'\r\n' +
'Third\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="field1"\r\n' +
'\r\n' +
'Joe Blow\r\nalmost tricked you!\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="field1"\r\n' +
'\r\n' +
'Repeated name segment\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n' +
'Content-Type: text/plain\r\n' +
'\r\n' +
'... contents of file1.txt ...\r\r\n' +
'--AaB03x--\r\n';
const server = Hapi.server();
server.route({ method: 'POST', path: '/echo', options: { handler: () => 'result', payload: { output: 'data', parse: true, maxBytes: 5, multipart: true } } });
const res = await server.inject({ method: 'POST', url: '/echo', payload: multipartPayload, simulate: { split: true }, headers: { 'content-length': null, 'content-type': 'multipart/form-data; boundary=AaB03x' } });
expect(res.statusCode).to.equal(400);
expect(res.payload.toString()).to.equal('{"statusCode":400,"error":"Bad Request","message":"Invalid multipart payload format"}');
});
it('errors if multipart disabled (default)', async () => {
const multipartPayload =
'--AaB03x\r\n' +
'content-disposition: form-data; name="x"\r\n' +
'\r\n' +
'First\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="x"\r\n' +
'\r\n' +
'Second\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="x"\r\n' +
'\r\n' +
'Third\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="field1"\r\n' +
'\r\n' +
'Joe Blow\r\nalmost tricked you!\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="field1"\r\n' +
'\r\n' +
'Repeated name segment\r\n' +
'--AaB03x\r\n' +
'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n' +
'Content-Type: text/plain\r\n' +
'\r\n' +
'... contents of file1.txt ...\r\r\n' +
'--AaB03x--\r\n';
const server = Hapi.server();
server.route({ method: 'POST', path: '/echo', options: { handler: () => 'result', payload: { output: 'data', parse: true, maxBytes: 5 } } });
const res = await server.inject({ method: 'POST', url: '/echo', payload: multipartPayload, simulate: { split: true }, headers: { 'content-length': null, 'content-type': 'multipart/form-data; boundary=AaB03x' } });
expect(res.statusCode).to.equal(415);
});
});
================================================
FILE: test/request.js
================================================
'use strict';
const Http = require('http');
const Net = require('net');
const Stream = require('stream');
const Url = require('url');
const Events = require('events');
const Boom = require('@hapi/boom');
const Code = require('@hapi/code');
const Hapi = require('..');
const Hoek = require('@hapi/hoek');
const Joi = require('joi');
const Lab = require('@hapi/lab');
const Teamwork = require('@hapi/teamwork');
const Wreck = require('@hapi/wreck');
const Common = require('./common');
const internals = {};
const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;
describe('Request.Generator', () => {
it('decorates request multiple times', async () => {
const server = Hapi.server();
server.decorate('request', 'x2', () => 2);
server.decorate('request', 'abc', () => 1);
server.route({
method: 'GET',
path: '/',
handler: (request) => {
return request.x2() + request.abc();
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal(3);
});
it('decorates request with non function method', async () => {
const server = Hapi.server();
const symbol = Symbol('abc');
server.decorate('request', 'x2', 2);
server.decorate('request', symbol, 1);
server.route({
method: 'GET',
path: '/',
handler: (request) => {
return request.x2 + request[symbol];
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal(3);
});
it('does not share decorations between servers via prototypes', async () => {
const server1 = Hapi.server();
const server2 = Hapi.server();
const route = {
method: 'GET',
path: '/',
handler: (request) => {
return Object.keys(Object.getPrototypeOf(request));
}
};
let res;
server1.decorate('request', 'x1', 1);
server2.decorate('request', 'x2', 2);
server1.route(route);
server2.route(route);
res = await server1.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal(['x1']);
res = await server2.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal(['x2']);
});
it('decorates symbols when apply=true', async () => {
const server = Hapi.server();
const symbol = Symbol('abc');
server.decorate('request', symbol, () => 'foo', { apply: true });
server.route({
method: 'GET',
path: '/',
handler: (request) => {
return request[symbol];
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('foo');
});
});
describe('Request', () => {
it('sets host and hostname', async () => {
const server = Hapi.server();
const handler = (request) => {
return [request.info.host, request.info.hostname].join('|');
};
server.route({ method: 'GET', path: '/', handler });
const res1 = await server.inject({ url: '/', headers: { host: 'host' } });
expect(res1.payload).to.equal('host|host');
const res2 = await server.inject({ url: '/', headers: { host: 'host:123' } });
expect(res2.payload).to.equal('host:123|host');
const res3 = await server.inject({ url: '/', headers: { host: '127.0.0.1' } });
expect(res3.payload).to.equal('127.0.0.1|127.0.0.1');
const res4 = await server.inject({ url: '/', headers: { host: '127.0.0.1:123' } });
expect(res4.payload).to.equal('127.0.0.1:123|127.0.0.1');
const res5 = await server.inject({ url: '/', headers: { host: '[::1]' } });
expect(res5.payload).to.equal('[::1]|[::1]');
const res6 = await server.inject({ url: '/', headers: { host: '[::1]:123' } });
expect(res6.payload).to.equal('[::1]:123|[::1]');
});
it('sets client address (default)', async (flags) => {
const server = Hapi.server();
const handler = (request) => {
// Call twice to reuse cached values
if (Common.hasIPv6) {
// 127.0.0.1 on node v14 and v16, ::1 on node v18 since DNS resolved to IPv6.
expect(request.info.remoteAddress).to.match(/^127\.0\.0\.1|::1$/);
expect(request.info.remoteAddress).to.match(/^127\.0\.0\.1|::1$/);
}
else {
expect(request.info.remoteAddress).to.equal('127.0.0.1');
expect(request.info.remoteAddress).to.equal('127.0.0.1');
}
expect(request.info.remotePort).to.be.above(0);
expect(request.info.remotePort).to.be.above(0);
return 'ok';
};
server.route({ method: 'get', path: '/', handler });
await server.start();
flags.onCleanup = () => server.stop();
const { payload } = await Wreck.get('http://localhost:' + server.info.port);
expect(payload.toString()).to.equal('ok');
});
it('sets client address (ipv4)', async (flags) => {
const server = Hapi.server();
const handler = (request) => {
Object.defineProperty(request.raw.req.socket, 'remoteAddress', {
value: '100.100.100.100'
});
return request.info.remoteAddress;
};
server.route({ method: 'get', path: '/', handler });
await server.start();
flags.onCleanup = () => server.stop();
const { payload } = await Wreck.get('http://localhost:' + server.info.port);
expect(payload.toString()).to.equal('100.100.100.100');
});
it('sets client address (ipv6)', async (flags) => {
const server = Hapi.server();
const handler = (request) => {
Object.defineProperty(request.raw.req.socket, 'remoteAddress', {
value: '::ffff:0:0:0:0:1'
});
return request.info.remoteAddress;
};
server.route({ method: 'get', path: '/', handler });
await server.start();
flags.onCleanup = () => server.stop();
const { payload } = await Wreck.get('http://localhost:' + server.info.port);
expect(payload.toString()).to.equal('::ffff:0:0:0:0:1');
});
it('sets client address (ipv4-mapped ipv6)', async (flags) => {
const server = Hapi.server();
const handler = (request) => {
Object.defineProperty(request.raw.req.socket, 'remoteAddress', {
value: '::ffff:100.100.100.100'
});
return request.info.remoteAddress;
};
server.route({ method: 'get', path: '/', handler });
await server.start();
flags.onCleanup = () => server.stop();
const { payload } = await Wreck.get('http://localhost:' + server.info.port);
expect(payload.toString()).to.equal('100.100.100.100');
});
it('sets client address to nothing when not available', async (flags) => {
const server = Hapi.server();
const abortedReqTeam = new Teamwork.Team();
let remoteAddr = 'not executed';
server.route({
method: 'GET',
path: '/',
options: {
handler: async (request, h) => {
req.destroy();
while (request.active()) {
await Hoek.wait(5);
}
abortedReqTeam.attend();
remoteAddr = request.info.remoteAddress;
return null;
}
}
});
await server.start();
flags.onCleanup = () => server.stop();
const req = Http.get(server.info.uri, Hoek.ignore);
req.on('error', Hoek.ignore);
await abortedReqTeam.work;
expect(remoteAddr).to.equal(undefined);
});
it('sets port to nothing when not available', async () => {
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler: (request) => request.info.remotePort === '' });
const res = await server.inject('/');
expect(res.result).to.equal(true);
});
it('sets referrer', async () => {
const server = Hapi.server();
const handler = (request) => {
expect(request.info.referrer).to.equal('http://site.com');
return 'ok';
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/', headers: { referrer: 'http://site.com' } });
expect(res.result).to.equal('ok');
});
it('sets referer', async () => {
const server = Hapi.server();
const handler = (request) => {
expect(request.info.referrer).to.equal('http://site.com');
return 'ok';
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/', headers: { referer: 'http://site.com' } });
expect(res.result).to.equal('ok');
});
it('sets acceptEncoding', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.info.acceptEncoding });
const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'gzip' } });
expect(res.result).to.equal('gzip');
});
it('handles invalid accept encoding header', async () => {
const server = Hapi.server({ routes: { log: { collect: true } } });
const handler = (request) => {
expect(request.logs[0].error.header).to.equal('a;b');
return request.info.acceptEncoding;
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'a;b' } });
expect(res.result).to.equal('identity');
});
it('sets headers', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.headers['user-agent'] });
const res = await server.inject('/');
expect(res.payload).to.equal('shot');
});
it('sets host info from :authority header when host header is absent', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => `${request.info.host}|${request.info.hostname}` });
const res = await server.inject({ url: '/', headers: { host: '', ':authority': 'example.com:8080' } });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('example.com:8080|example.com');
});
it('generates unique request id', async () => {
const server = Hapi.server();
server._core.requestCounter = { value: 10, min: 10, max: 11 };
server.route({ method: 'GET', path: '/', handler: (request) => request.info.id });
const res1 = await server.inject('/');
expect(res1.result).to.match(/10$/);
const res2 = await server.inject('/');
expect(res2.result).to.match(/11$/);
const res3 = await server.inject('/');
expect(res3.result).to.match(/10$/);
});
it('can serialize request.info with JSON.stringify()', async () => {
const server = Hapi.server();
const handler = (request) => {
const actual = JSON.stringify(request.info);
const expected = JSON.stringify({
acceptEncoding: request.info.acceptEncoding,
completed: request.info.completed,
cors: request.info.cors,
host: request.info.host,
hostname: request.info.hostname,
id: request.info.id,
received: request.info.received,
referrer: request.info.referrer,
remoteAddress: request.info.remoteAddress,
remotePort: request.info.remotePort,
responded: request.info.responded
});
expect(actual).to.equal(expected);
return 'ok';
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/' });
expect(res.result).to.equal('ok');
});
describe('active()', () => {
it('exits handler early when request is no longer active', { retry: true }, async (flags) => {
let testComplete = false;
const onCleanup = [];
flags.onCleanup = async () => {
testComplete = true;
for (const cleanup of onCleanup) {
await cleanup();
}
};
const server = Hapi.server();
const leaveHandlerTeam = new Teamwork.Team();
server.route({
method: 'GET',
path: '/',
options: {
handler: async (request, h) => {
req.destroy();
while (request.active() && !testComplete) {
await Hoek.wait(10);
}
leaveHandlerTeam.attend({
active: request.active(),
testComplete
});
return null;
}
}
});
await server.start();
onCleanup.unshift(() => server.stop());
const req = Http.get(server.info.uri, Hoek.ignore);
req.on('error', Hoek.ignore);
const note = await leaveHandlerTeam.work;
expect(note).to.equal({
active: false,
testComplete: false
});
});
});
describe('_execute()', () => {
it('returns 400 on invalid path', async () => {
const server = Hapi.server();
server.ext('onRequest', (request, h) => {
expect(request.url).to.be.null();
expect(request.query).to.equal({});
expect(request.path).to.equal('invalid');
return h.continue;
});
const res = await server.inject('invalid');
expect(res.statusCode).to.equal(400);
expect(res.result.message).to.startWith('Invalid URL');
});
it('returns boom response on ext error', async () => {
const server = Hapi.server();
const ext = (request) => {
throw Boom.badRequest();
};
server.ext('onPostHandler', ext);
server.route({ method: 'GET', path: '/', handler: () => 'OK' });
const res = await server.inject('/');
expect(res.result.statusCode).to.equal(400);
});
it('returns error response on ext error', async () => {
const server = Hapi.server();
const ext = (request) => {
throw new Error('oops');
};
server.ext('onPostHandler', ext);
server.route({ method: 'GET', path: '/', handler: () => 'OK' });
const res = await server.inject('/');
expect(res.result.statusCode).to.equal(500);
});
it('returns error response on ext timeout', async () => {
const server = Hapi.server();
const responded = server.ext('onPostResponse');
const ext = (request) => {
return Hoek.block();
};
server.ext('onPostHandler', ext, { timeout: 100 });
server.route({ method: 'GET', path: '/', handler: () => 'OK' });
const res = await server.inject('/');
expect(res.result.statusCode).to.equal(500);
const request = await responded;
expect(request.response._error).to.be.an.error('onPostHandler timed out');
});
it('logs error responses on onPostResponse ext error', async () => {
const server = Hapi.server();
const ext1 = () => {
throw new Error('oops1');
};
server.ext('onPostResponse', ext1);
const ext2 = () => {
throw new Error('oops2');
};
server.ext('onPostResponse', ext2);
server.route({ method: 'GET', path: '/', handler: () => 'OK' });
const log = server.events.few({ name: 'request', channels: 'internal', filter: 'ext', count: 2 });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
const [[, event1], [, event2]] = await log;
expect(event1.error).to.be.an.error('oops1');
expect(event2.error).to.be.an.error('oops2');
});
it('handles aborted requests (during response)', async () => {
const handler = (request) => {
const TestStream = class extends Stream.Readable {
_read(size) {
if (this.isDone) {
return;
}
this.isDone = true;
this.push('success');
this.emit('data', 'success');
}
};
const stream = new TestStream();
return stream;
};
const server = Hapi.server({ info: { remote: true } });
server.route({ method: 'GET', path: '/', handler });
let disconnected = 0;
let info;
const onRequest = (request, h) => {
request.events.once('disconnect', () => {
info = request.info;
++disconnected;
});
return h.continue;
};
server.ext('onRequest', onRequest);
await server.start();
let total = 2;
const createConnection = function () {
const client = Net.connect(server.info.port, () => {
client.write('GET / HTTP/1.1\r\nHost: host\r\n\r\n');
client.write('GET / HTTP/1.1\r\nHost: host\r\n\r\n');
});
client.on('data', () => {
--total;
client.destroy();
});
};
await new Promise((resolve) => {
const check = function () {
if (total) {
createConnection();
setTimeout(check, 100);
}
else {
expect(disconnected).to.equal(4); // Each connection sends two HTTP requests
resolve();
}
};
check();
});
await server.stop();
expect(info.remotePort).to.exist();
expect(info.remoteAddress).to.exist();
});
it('handles aborted requests (before response)', { retry: true }, async (flags) => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/test',
handler: () => null
});
const codes = [];
server.ext('onPostResponse', (request) => codes.push(Boom.isBoom(request.response) ? request.response.output.statusCode : request.response.statusCode));
const team = new Teamwork.Team();
const onRequest = (request, h) => {
request.events.once('disconnect', () => team.attend());
return h.continue;
};
server.ext('onRequest', onRequest);
let firstRequest = true;
const onPreHandler = async (request, h) => {
if (firstRequest) {
client.destroy();
firstRequest = false;
}
else {
// To avoid timing differences between node versions, ensure that
// the second and third requests always experience the disconnect
await team.work;
}
return h.continue;
};
server.ext('onPreHandler', onPreHandler);
await server.start();
flags.onCleanup = () => server.stop();
const client = Net.connect(server.info.port, () => {
client.write('GET /test HTTP/1.1\r\nHost: host\r\n\r\n');
client.write('GET /test HTTP/1.1\r\nHost: host\r\n\r\n');
client.write('GET /test HTTP/1.1\r\nHost: host\r\n\r\n');
});
await team.work;
await server.stop();
expect(codes).to.equal([204, 499, 499]);
});
it('returns empty params array when none present', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.params });
const res = await server.inject('/');
expect(res.result).to.equal({});
});
it('returns empty params array when none present (not found)', async () => {
const server = Hapi.server();
const preResponse = (request) => {
return request.params;
};
server.ext('onPreResponse', preResponse);
const res = await server.inject('/');
expect(res.result).to.equal({});
});
it('does not fail on abort', async () => {
const server = Hapi.server();
const team = new Teamwork.Team();
const handler = async (request) => {
clientRequest.destroy();
await Hoek.wait(10);
team.attend();
throw new Error('fail');
};
server.route({ method: 'GET', path: '/', handler });
await server.start();
const clientRequest = Http.request({
hostname: 'localhost',
port: server.info.port,
method: 'GET'
});
clientRequest.on('error', Hoek.ignore);
clientRequest.end();
await team.work;
await server.stop();
});
it('does not fail on abort (onPreHandler)', async () => {
const server = Hapi.server();
const team = new Teamwork.Team();
server.route({ method: 'GET', path: '/', handler: () => null });
const preHandler = async (request, h) => {
clientRequest.destroy();
await Hoek.wait(10);
team.attend();
return h.continue;
};
server.ext('onPreHandler', preHandler);
await server.start();
const clientRequest = Http.request({
hostname: 'localhost',
port: server.info.port,
method: 'GET'
});
clientRequest.on('error', Hoek.ignore);
clientRequest.end();
await team.work;
await server.stop();
});
it('does not fail on abort with ext', async () => {
const handler = async (request) => {
clientRequest.destroy();
await Hoek.wait(10);
throw new Error('boom');
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const preResponse = (request, h) => {
return h.continue;
};
server.ext('onPreResponse', preResponse);
const log = server.events.once('response');
await server.start();
const clientRequest = Http.request({
hostname: 'localhost',
port: server.info.port,
method: 'GET'
});
clientRequest.on('error', Hoek.ignore);
clientRequest.end();
await log;
await server.stop();
});
it('returns not found on internal only route (external)', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/some/route',
options: {
isInternal: true,
handler: () => 'ok'
}
});
await server.start();
const err = await expect(Wreck.get('http://localhost:' + server.info.port)).to.reject();
expect(err.data.res.statusCode).to.equal(404);
expect(err.data.payload.toString()).to.equal('{"statusCode":404,"error":"Not Found","message":"Not Found"}');
await server.stop();
});
it('returns not found on internal only route (inject)', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/some/route',
options: {
isInternal: true,
handler: () => 'ok'
}
});
const res = await server.inject('/some/route');
expect(res.statusCode).to.equal(404);
});
it('allows internal only route (inject with allowInternals)', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/some/route',
options: {
isInternal: true,
handler: () => 'ok'
}
});
const res = await server.inject({ url: '/some/route', allowInternals: true });
expect(res.statusCode).to.equal(200);
});
it('allows internal only route (inject with allowInternals and authority)', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/some/route',
options: {
isInternal: true,
handler: () => 'ok'
}
});
const res = await server.inject({ url: '/some/route', allowInternals: true, authority: 'server:8000' });
expect(res.statusCode).to.equal(200);
});
it('creates arrays from multiple entries', async () => {
const server = Hapi.server();
const handler = (request) => {
return { a: request.query.a, array: Array.isArray(request.query.a), instance: request.query.a instanceof Array };
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/?a=1&a=2');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal({ a: ['1', '2'], array: true, instance: true });
});
it('supports custom query parser (new object)', async () => {
const parser = (query) => {
return { hello: query.hi };
};
const server = Hapi.server({ query: { parser } });
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => request.query.hello
}
});
const res = await server.inject('/?hi=hola');
expect(res.statusCode).to.equal(200);
expect(res.payload).to.equal('hola');
});
it('supports custom query parser (same object)', async () => {
const parser = (query) => {
query.hello = query.hi;
return query;
};
const server = Hapi.server({ query: { parser } });
server.route({
method: 'GET', path: '/', options: {
handler: (request) => request.query.hello
}
});
const res = await server.inject('/?hi=hola');
expect(res.statusCode).to.equal(200);
expect(res.payload).to.equal('hola');
});
it('returns 500 when custom query parser returns non-object', async () => {
const server = Hapi.server({ debug: false, query: { parser: () => 'something' } });
server.route({
method: 'GET', path: '/', options: {
handler: (request) => request.query.hello
}
});
const res = await server.inject('/?hi=hola');
expect(res.statusCode).to.equal(500);
expect(res.request.response._error).to.be.an.error('Parsed query must be an object');
});
it('returns 500 when custom query parser returns null', async () => {
const server = Hapi.server({ debug: false, query: { parser: () => null } });
server.route({
method: 'GET', path: '/', options: {
handler: (request) => request.query.hello
}
});
const res = await server.inject('/?hi=hola');
expect(res.statusCode).to.equal(500);
expect(res.request.response._error).to.be.an.error('Parsed query must be an object');
});
});
describe('_onRequest()', () => {
it('errors on non-takeover response', async () => {
const server = Hapi.server({ debug: false });
server.ext('onRequest', () => 'something');
server.route({ method: 'GET', path: '/', handler: () => null });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
});
describe('_lifecycle()', () => {
it('errors on non-takeover response in pre handler ext', async () => {
const server = Hapi.server({ debug: false });
server.ext('onPreHandler', () => 'something');
server.route({ method: 'GET', path: '/', handler: () => null });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
it('logs thrown errors as boom errors', async () => {
const server = Hapi.server({ debug: false });
server.route({
method: 'GET',
path: '/',
options: {
handler: function () {
// eslint-disable-next-line no-undef
NOT_DEFINED_VAR;
}
}
});
const log = new Promise((resolve) => {
server.events.on({ name: 'request', channels: 'internal' }, (request, event, tags) => {
if (tags.handler &&
tags.error) {
resolve({ event, tags });
}
});
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
const { event } = await log;
expect(event.error.isBoom).to.equal(true);
expect(event.error.output.statusCode).to.equal(500);
expect(event.error.stack).to.exist();
});
});
describe('_postCycle()', () => {
it('skips onPreResponse when validation terminates request', { retry: true }, async (flags) => {
const server = Hapi.server();
const abortedReqTeam = new Teamwork.Team();
let called = false;
server.ext('onPreResponse', (request, h) => {
called = true;
return h.continue;
});
server.route({
method: 'GET',
path: '/',
options: {
handler: (request) => {
// Stash raw so that we can access it on response validation
Object.assign(request.app, request.raw);
return null;
},
response: {
status: {
200: async (_, { context }) => {
req.destroy();
const raw = context.app.request;
await Events.once(raw.req, 'aborted');
abortedReqTeam.attend();
}
}
}
}
});
await server.start();
flags.onCleanup = () => server.stop();
const req = Http.get(server.info.uri, Hoek.ignore);
req.on('error', Hoek.ignore);
await abortedReqTeam.work;
await server.events.once('response');
expect(called).to.be.false();
});
it('handles continue signal', async () => {
const server = Hapi.server({ debug: false });
server.route({
method: 'GET',
path: '/',
options: {
handler: () => ({ a: '1' }),
validate: {
validator: Joi
},
response: {
failAction: (request, h) => h.continue,
schema: {
b: Joi.string()
}
}
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
});
});
describe('_reply()', () => {
it('returns a reply with auto end in onPreResponse', async () => {
const server = Hapi.server();
server.ext('onPreResponse', (request, h) => h.close);
server.route({ method: 'GET', path: '/', handler: () => null });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('');
});
});
describe('_finalize()', () => {
it('generate response event', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
const log = server.events.once('response');
await server.inject('/');
const [request] = await log;
expect(request.info.responded).to.be.min(request.info.received);
expect(request.info.completed).to.be.min(request.info.responded);
expect(request.response.source).to.equal('ok');
expect(request.response.statusCode).to.equal(200);
});
it('skips logging error when not the result of a thrown error', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response().code(500) });
let called = false;
server.events.once('request', () => {
called = true;
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
expect(res.request.response._error).to.not.exist();
expect(called).to.be.false();
});
it('destroys response after server timeout', async () => {
const team = new Teamwork.Team();
const handler = async (request) => {
await Hoek.wait(100);
const stream = new Stream.Readable();
stream._read = function (size) {
this.push('value');
this.push(null);
};
stream._destroy = () => team.attend();
return stream;
};
const server = Hapi.server({ routes: { timeout: { server: 50 } } });
server.route({
method: 'GET',
path: '/',
handler
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(503);
await team.work;
});
it('does not attempt to close error response after server timeout', async () => {
const handler = async (request) => {
await Hoek.wait(40);
throw new Error('after');
};
const server = Hapi.server({ routes: { timeout: { server: 20 } } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(503);
});
it('emits request-error once', async () => {
const server = Hapi.server({ debug: false, routes: { log: { collect: true } } });
let errs = 0;
let req = null;
server.events.on({ name: 'request', channels: 'error' }, (request, { error }) => {
errs++;
expect(error).to.exist();
expect(error.message).to.equal('boom2');
req = request;
});
const preResponse = (request) => {
throw new Error('boom2');
};
server.ext('onPreResponse', preResponse);
const handler = (request) => {
throw new Error('boom1');
};
server.route({ method: 'GET', path: '/', handler });
const log = server.events.once('response');
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
expect(res.result).to.exist();
expect(res.result.message).to.equal('An internal server error occurred');
await log;
expect(errs).to.equal(1);
expect(req.logs[1].tags).to.equal(['internal', 'error']);
});
it('does not emit request-error when error is replaced with valid response', async () => {
const server = Hapi.server({ debug: false });
let errs = 0;
server.events.on({ name: 'request', channels: 'error' }, (request, event) => {
errs++;
});
server.ext('onPreResponse', () => 'ok');
const handler = (request) => {
throw new Error('boom1');
};
server.route({ method: 'GET', path: '/', handler });
const log = server.events.once('response');
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('ok');
await log;
expect(errs).to.equal(0);
});
});
describe('setMethod()', () => {
it('changes method with a lowercase version of the value passed in', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => null });
const onRequest = (request, h) => {
request.setMethod('POST');
return h.response(request.method).takeover();
};
server.ext('onRequest', onRequest);
const res = await server.inject('/');
expect(res.payload).to.equal('post');
});
it('errors on missing method', async () => {
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler: () => null });
server.ext('onRequest', (request) => request.setMethod());
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
it('errors on invalid method type', async () => {
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler: () => null });
server.ext('onRequest', (request) => request.setMethod(42));
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
});
describe('setUrl()', () => {
it('sets url, path, and query', async () => {
const url = 'http://localhost/page?param1=something';
const server = Hapi.server();
const handler = (request) => {
return [request.url.href, request.path, request.query.param1].join('|');
};
server.route({ method: 'GET', path: '/page', handler });
const onRequest = (request, h) => {
request.setUrl(url);
return h.continue;
};
server.ext('onRequest', onRequest);
const res = await server.inject('/');
expect(res.payload).to.equal(url + '|/page|something');
});
it('sets root url', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: (request) => request.url.pathname });
const onRequest = (request, h) => {
request.setUrl('/');
return h.continue;
};
server.ext('onRequest', onRequest);
const res = await server.inject('/a/b/c');
expect(res.result).to.equal('/');
});
it('updates host info', async () => {
const url = 'http://redirected:321/';
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => null });
const onRequest = (request, h) => {
const initialHost = request.info.host;
request.setUrl(url);
return h.response([request.url.href, request.path, initialHost, request.info.host, request.info.hostname].join('|')).takeover();
};
server.ext('onRequest', onRequest);
const res = await server.inject({ url: '/', headers: { host: 'initial:123' } });
expect(res.payload).to.equal(url + '|/|initial:123|redirected:321|redirected');
});
it('updates host info when set without port number', async () => {
const url = 'http://redirected/';
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => null });
const onRequest = (request, h) => {
const initialHost = request.info.host;
request.setUrl(url);
return h.response([request.url.href, request.path, initialHost, request.info.host, request.info.hostname].join('|')).takeover();
};
server.ext('onRequest', onRequest);
const res1 = await server.inject({ url: '/', headers: { host: 'initial:123' } });
const res2 = await server.inject({ url: '/', headers: { host: 'initial' } });
expect(res1.payload).to.equal(url + '|/|initial:123|redirected|redirected');
expect(res2.payload).to.equal(url + '|/|initial|redirected|redirected');
});
it('overrides query string content', async () => {
const server = Hapi.server();
const handler = (request) => {
return [request.url.href, request.path, request.query.a].join('|');
};
server.route({ method: 'GET', path: '/', handler });
const onRequest = (request, h) => {
const uri = request.raw.req.url;
const parsed = new Url.URL(uri, 'http://test/');
parsed.searchParams.set('a', 2);
request.setUrl(parsed);
return h.continue;
};
server.ext('onRequest', onRequest);
const res = await server.inject('/?a=1');
expect(res.payload).to.equal('http://test/?a=2|/|2');
});
it('normalizes a path', async () => {
const rawPath = '/%0%1%2%3%4%5%6%7%8%9%a%b%c%d%e%f%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f%20%21%22%23%24%25%26%27%28%29%2a%2b%2c%2d%2e%2f%30%31%32%33%34%35%36%37%38%39%3a%3b%3c%3d%3e%3f%40%41%42%43%44%45%46%47%48%49%4a%4b%4c%4d%4e%4f%50%51%52%53%54%55%56%57%58%59%5a%5b%5c%5d%5e%5f%60%61%62%63%64%65%66%67%68%69%6a%6b%6c%6d%6e%6f%70%71%72%73%74%75%76%77%78%79%7a%7b%7c%7d%7e%7f%80%81%82%83%84%85%86%87%88%89%8a%8b%8c%8d%8e%8f%90%91%92%93%94%95%96%97%98%99%9a%9b%9c%9d%9e%9f%a0%a1%a2%a3%a4%a5%a6%a7%a8%a9%aa%ab%ac%ad%ae%af%b0%b1%b2%b3%b4%b5%b6%b7%b8%b9%ba%bb%bc%bd%be%bf%c0%c1%c2%c3%c4%c5%c6%c7%c8%c9%ca%cb%cc%cd%ce%cf%d0%d1%d2%d3%d4%d5%d6%d7%d8%d9%da%db%dc%dd%de%df%e0%e1%e2%e3%e4%e5%e6%e7%e8%e9%ea%eb%ec%ed%ee%ef%f0%f1%f2%f3%f4%f5%f6%f7%f8%f9%fa%fb%fc%fd%fe%ff%0%1%2%3%4%5%6%7%8%9%A%B%C%D%E%F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F%40%41%42%43%44%45%46%47%48%49%4A%4B%4C%4D%4E%4F%50%51%52%53%54%55%56%57%58%59%5A%5B%5C%5D%5E%5F%60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D%6E%6F%70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF';
const normPath = '/%0%1%2%3%4%5%6%7%8%9%a%b%c%d%e%f%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20!%22%23$%25&\'()*+,-.%2F0123456789:;%3C=%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF%0%1%2%3%4%5%6%7%8%9%A%B%C%D%E%F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20!%22%23$%25&\'()*+,-.%2F0123456789:;%3C=%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF';
const url = 'http://localhost' + rawPath + '?param1=something';
const normUrl = 'http://localhost' + normPath + '?param1=something';
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler: () => null });
const onRequest = (request, h) => {
request.setUrl(url);
return h.response([request.url.href, request.path, request.url.searchParams.get('param1')].join('|')).takeover();
};
server.ext('onRequest', onRequest);
const res = await server.inject('/');
expect(res.payload).to.equal(normUrl + '|' + normPath + '|something');
});
it('errors on empty path', async () => {
const server = Hapi.server({ debug: false });
const onRequest = (request, h) => {
request.setUrl('');
return h.continue;
};
server.ext('onRequest', onRequest);
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
it('throws when path is missing', async () => {
const server = Hapi.server();
const onRequest = (request, h) => {
try {
request.setUrl();
}
catch (err) {
return h.response(err.message).takeover();
}
return h.continue;
};
server.ext('onRequest', onRequest);
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.payload).to.equal('Url must be a string or URL object');
});
it('strips trailing slash', async () => {
const server = Hapi.server({ router: { stripTrailingSlash: true } });
server.route({ method: 'GET', path: '/test', handler: () => null });
const res1 = await server.inject('/test/');
expect(res1.statusCode).to.equal(204);
const res2 = await server.inject('/test');
expect(res2.statusCode).to.equal(204);
});
it('does not strip trailing slash on /', async () => {
const server = Hapi.server({ router: { stripTrailingSlash: true } });
server.route({ method: 'GET', path: '/', handler: () => null });
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
});
it('strips trailing slash with query', async () => {
const server = Hapi.server({ router: { stripTrailingSlash: true } });
server.route({ method: 'GET', path: '/test', handler: () => null });
const res = await server.inject('/test/?a=b');
expect(res.statusCode).to.equal(204);
});
it('clones passed url', async () => {
const urlObject = new Url.URL('http:/%41');
let requestUrl;
const server = Hapi.server();
const onRequest = (request, h) => {
request.setUrl(urlObject);
requestUrl = request.url;
return h.continue;
};
server.ext('onRequest', onRequest);
const res = await server.inject('/');
expect(res.statusCode).to.equal(404);
expect(requestUrl).to.equal(urlObject);
expect(requestUrl).to.not.shallow.equal(urlObject);
});
it('handles vhost redirection', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/', vhost: 'one', handler: () => 'success' });
const onRequest = (request, h) => {
request.setUrl('http://one/');
return h.continue;
};
server.ext('onRequest', onRequest);
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.payload).to.equal('success');
});
it('handles hostname in HTTP request resource', async () => {
const server = Hapi.server({ debug: false });
const team = new Teamwork.Team();
let hostname;
server.route({
method: 'GET',
path: '/',
handler: (request) => {
hostname = request.info.hostname;
team.attend();
return null;
}
});
await server.start();
const socket = Net.createConnection(server.info.port, '127.0.0.1', () => socket.write('GET http://host.com\r\n\r\n'));
await team.work;
socket.destroy();
await server.stop();
expect(hostname).to.equal('host.com');
});
it('handles url starting with multiple /', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/{p*}',
handler: (request) => {
return {
p: request.params.p,
path: request.path,
hostname: request.info.hostname.toLowerCase() // Lowercase for OSX tests
};
}
});
const res = await server.inject('//path');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal({ p: '/path', path: '//path', hostname: server.info.host.toLowerCase() });
});
it('handles escaped path segments', async () => {
const server = Hapi.server();
server.route({ path: '/%2F/%2F', method: 'GET', handler: (request) => request.path });
const tests = [
['/', 404],
['////', 404],
['/%2F/%2F', 200, '/%2F/%2F'],
['/%2F/%2F#x', 200, '/%2F/%2F'],
['/%2F/%2F?a=1#x', 200, '/%2F/%2F']
];
for (const [uri, code, result] of tests) {
const res = await server.inject(uri);
expect(res.statusCode).to.equal(code);
if (code < 400) {
expect(res.result).to.equal(result);
}
}
});
it('handles fragments (no query)', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/{p*}', handler: (request) => request.path });
await server.start();
const options = {
hostname: 'localhost',
port: server.info.port,
path: '/path#ignore',
method: 'GET'
};
const team = new Teamwork.Team();
const req = Http.request(options, (res) => team.attend(res));
req.end();
const res = await team.work;
const payload = await Wreck.read(res);
expect(payload.toString()).to.equal('/path');
await server.stop();
});
it('handles fragments (with query)', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/{p*}', handler: (request) => request.query.a });
await server.start();
const options = {
hostname: 'localhost',
port: server.info.port,
path: '/path?a=1#ignore',
method: 'GET'
};
const team = new Teamwork.Team();
const req = Http.request(options, (res) => team.attend(res));
req.end();
const res = await team.work;
const payload = await Wreck.read(res);
expect(payload.toString()).to.equal('1');
await server.stop();
});
it('handles fragments with ? (no query)', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/{p*}', handler: (request) => request.path });
await server.start();
const options = {
hostname: 'localhost',
port: server.info.port,
path: '/path#ignore?x',
method: 'GET'
};
const team = new Teamwork.Team();
const req = Http.request(options, (res) => team.attend(res));
req.end();
const res = await team.work;
const payload = await Wreck.read(res);
expect(payload.toString()).to.equal('/path');
await server.stop();
});
it('handles absolute URL (proxy)', async () => {
const server = Hapi.server();
server.route({ method: 'GET', path: '/{p*}', handler: (request) => request.query.a.join() });
await server.start();
const options = {
hostname: 'localhost',
port: server.info.port,
path: 'http://example.com/path?a=1&a=2#ignore',
method: 'GET'
};
const team = new Teamwork.Team();
const req = Http.request(options, (res) => team.attend(res));
req.end();
const res = await team.work;
const payload = await Wreck.read(res);
expect(payload.toString()).to.equal('1,2');
await server.stop();
});
});
describe('url', () => {
it('generates URL object lazily', async () => {
const server = Hapi.server();
const handler = (request) => {
expect(request._url).to.not.exist();
return request.url.pathname;
};
server.route({ path: '/test', method: 'GET', handler });
const res = await server.inject('/test?a=1');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('/test');
});
it('generates URL object lazily (no host header)', async () => {
const server = Hapi.server();
const handler = (request) => {
delete request.info.host;
expect(request._url).to.not.exist();
return request.url.pathname;
};
server.route({ path: '/test', method: 'GET', handler });
const res = await server.inject('/test?a=1');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('/test');
});
it('generates valid URL when server host is IPv6 and host header is absent', async () => {
const server = Hapi.server({ host: '::1' });
const handler = (request) => {
delete request.info.host;
expect(request._url).to.not.exist();
return request.url.host;
};
server.route({ path: '/test', method: 'GET', handler });
const res = await server.inject('/test');
expect(res.statusCode).to.equal(200);
expect(res.result).to.match(/^\[::1\]:\d+$/);
});
});
describe('_tap()', () => {
it('listens to request payload read finish', async () => {
let finish;
const ext = (request, h) => {
finish = request.events.once('finish');
return h.continue;
};
const server = Hapi.server();
server.ext('onRequest', ext);
server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { parse: false } } });
const payload = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789';
await server.inject({ method: 'POST', url: '/', payload });
await finish;
});
it('ignores emitter when created for other events', async () => {
const ext = (request, h) => {
request.events;
return h.continue;
};
const server = Hapi.server();
server.ext('onRequest', ext);
server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { parse: false } } });
const payload = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789';
await server.inject({ method: 'POST', url: '/', payload });
});
});
describe('log()', () => {
it('outputs log data to debug console', async () => {
const handler = (request) => {
request.log(['implementation'], 'data');
return null;
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const log = new Promise((resolve) => {
const orig = console.error;
console.error = function (...args) {
expect(args[0]).to.equal('Debug:');
expect(args[1]).to.equal('implementation');
expect(args[2]).to.equal('\n data');
console.error = orig;
resolve();
};
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
await log;
});
it('emits a request event', async () => {
const server = Hapi.server();
const handler = async (request) => {
const log = server.events.once({ name: 'request', channels: 'app' });
request.log(['test'], 'data');
const [, event, tags] = await log;
expect(event).to.contain(['request', 'timestamp', 'tags', 'data', 'channel']);
expect(event.data).to.equal('data');
expect(event.channel).to.equal('app');
expect(tags).to.equal({ test: true });
return null;
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
});
it('emits a request event (function data + collect)', async () => {
const server = Hapi.server({ routes: { log: { collect: true } } });
const handler = async (request) => {
const log = server.events.once('request');
request.log(['test'], () => 'data');
const [, event, tags] = await log;
expect(event).to.contain(['request', 'timestamp', 'tags', 'data', 'channel']);
expect(event.data).to.equal('data');
expect(event.channel).to.equal('app');
expect(tags).to.equal({ test: true });
expect(request.logs[0].data).to.equal('data');
return null;
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
});
it('emits a request event (function data)', async () => {
const server = Hapi.server();
const handler = async (request) => {
const log = server.events.once('request');
request.log(['test'], () => 'data');
const [, event, tags] = await log;
expect(event).to.contain(['request', 'timestamp', 'tags', 'data', 'channel']);
expect(event.data).to.equal('data');
expect(event.channel).to.equal('app');
expect(tags).to.equal({ test: true });
return null;
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
});
it('outputs log to debug console without data', async () => {
const handler = (request) => {
request.log(['implementation']);
return null;
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const log = new Promise((resolve) => {
const orig = console.error;
console.error = function (...args) {
expect(args[0]).to.equal('Debug:');
expect(args[1]).to.equal('implementation');
expect(args[2]).to.equal('');
console.error = orig;
resolve();
};
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
await log;
});
it('outputs log to debug console with error data', async () => {
const handler = (request) => {
request.log(['implementation'], new Error('boom'));
return null;
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const log = new Promise((resolve) => {
const orig = console.error;
console.error = function (...args) {
expect(args[0]).to.equal('Debug:');
expect(args[1]).to.equal('implementation');
expect(args[2]).to.contain('Error: boom');
console.error = orig;
resolve();
};
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
await log;
});
it('handles invalid log data object stringify', async () => {
const handler = (request) => {
const obj = {};
obj.a = obj;
request.log(['implementation'], obj);
return null;
};
const server = Hapi.server({ routes: { log: { collect: true } } });
server.route({ method: 'GET', path: '/', handler });
const log = new Promise((resolve) => {
const orig = console.error;
console.error = function (...args) {
expect(args[0]).to.equal('Debug:');
expect(args[1]).to.equal('implementation');
expect(args[2]).to.match(/Cannot display object: Converting circular structure to JSON/);
console.error = orig;
resolve();
};
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
await log;
});
it('adds a log event to the request', async () => {
const handler = (request) => {
request.log('1', 'log event 1');
request.log(['2'], 'log event 2');
request.log(['3', '4']);
request.log(['1', '4']);
request.log(['2', '3']);
request.log(['4']);
request.log('4');
return request.logs.map((event) => event.tags).join('|');
};
const server = Hapi.server({ routes: { log: { collect: true } } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.payload).to.equal('1|2|3,4|1,4|2,3|4|4');
});
it('does not output events when debug disabled', async () => {
const server = Hapi.server({ debug: false });
let i = 0;
const orig = console.error;
console.error = function () {
++i;
};
const handler = (request) => {
request.log(['implementation']);
return null;
};
server.route({ method: 'GET', path: '/', handler });
await server.inject('/');
console.error('nothing');
expect(i).to.equal(1);
console.error = orig;
});
it('does not output events when debug.request disabled', async () => {
const server = Hapi.server({ debug: { request: false } });
let i = 0;
const orig = console.error;
console.error = function () {
++i;
};
const handler = (request) => {
request.log(['implementation']);
return null;
};
server.route({ method: 'GET', path: '/', handler });
await server.inject('/');
console.error('nothing');
expect(i).to.equal(1);
console.error = orig;
});
it('does not output non-implementation events by default', async () => {
const server = Hapi.server();
let i = 0;
const orig = console.error;
console.error = function () {
++i;
};
const handler = (request) => {
request.log(['xyz']);
return null;
};
server.route({ method: 'GET', path: '/', handler });
await server.inject('/');
console.error('nothing');
expect(i).to.equal(1);
console.error = orig;
});
it('logs nothing', async () => {
const server = Hapi.server({ debug: false, routes: { log: { collect: false } } });
const handler = (request) => {
expect(request.logs).to.have.length(0);
return request.info.acceptEncoding;
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'a;b' } });
expect(res.result).to.equal('identity');
});
it('logs when only collect is true', async () => {
const server = Hapi.server({ debug: false, routes: { log: { collect: true } } });
const handler = (request) => {
expect(request.logs).to.have.length(1);
return request.info.acceptEncoding;
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'a;b' } });
expect(res.result).to.equal('identity');
});
});
describe('_setResponse()', () => {
it('leaves the response open when the same response is set again', async () => {
const server = Hapi.server();
const postHandler = (request) => {
return request.response;
};
server.ext('onPostHandler', postHandler);
const handler = (request) => {
const stream = new Stream.Readable();
stream._read = function (size) {
this.push('value');
this.push(null);
};
return stream;
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('value');
});
it('leaves the response open when the same response source is set again', async () => {
const server = Hapi.server();
server.ext('onPostHandler', (request) => request.response.source);
const handler = (request) => {
const stream = new Stream.Readable();
stream._read = function (size) {
this.push('value');
this.push(null);
};
return stream;
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('value');
});
});
describe('timeout', () => {
it('returns server error message when server taking too long', async () => {
const handler = async (request) => {
await Hoek.wait(100);
return 'too slow';
};
const server = Hapi.server({ routes: { timeout: { server: 50 } } });
server.route({ method: 'GET', path: '/timeout', handler });
const timer = new Hoek.Bench();
const res = await server.inject('/timeout');
expect(res.statusCode).to.equal(503);
expect(timer.elapsed()).to.be.at.least(49);
});
it('returns server error message when server timeout happens during request execution (and handler yields)', async () => {
const handler = async (request) => {
await Hoek.wait(20);
return null;
};
const server = Hapi.server({ routes: { timeout: { server: 10 } } });
server.route({ method: 'GET', path: '/', options: { handler } });
const postHandler = (request, h) => {
return h.continue;
};
server.ext('onPostHandler', postHandler);
const res = await server.inject('/');
expect(res.statusCode).to.equal(503);
});
it('returns server error message when server timeout is short and already occurs when request executes', async () => {
const server = Hapi.server({ routes: { timeout: { server: 2 } } });
server.route({ method: 'GET', path: '/', options: { handler: function () { } } });
const onRequest = async (request, h) => {
await Hoek.wait(10);
return h.continue;
};
server.ext('onRequest', onRequest);
const res = await server.inject('/');
expect(res.statusCode).to.equal(503);
});
it('handles server handler timeout with onPreResponse ext', async () => {
const handler = async (request) => {
await Hoek.wait(20);
return null;
};
const server = Hapi.server({ routes: { timeout: { server: 10 } } });
server.route({ method: 'GET', path: '/', options: { handler } });
const preResponse = (request, h) => {
return h.continue;
};
server.ext('onPreResponse', preResponse);
const res = await server.inject('/');
expect(res.statusCode).to.equal(503);
});
it('does not return an error response when server is slow but faster than timeout', async () => {
const slowHandler = async (request) => {
await Hoek.wait(30);
return 'slow';
};
const server = Hapi.server({ routes: { timeout: { server: 50 } } });
server.route({ method: 'GET', path: '/slow', options: { handler: slowHandler } });
const timer = new Hoek.Bench();
const res = await server.inject('/slow');
expect(timer.elapsed()).to.be.at.least(20);
expect(res.statusCode).to.equal(200);
});
it('creates error response when request is aborted while draining payload', async () => {
const server = Hapi.server({ routes: { timeout: { server: false } } });
await server.start();
const log = server.events.once('response');
const ready = new Promise((resolve) => {
server.ext('onRequest', (request, h) => {
resolve();
return h.continue;
});
});
const req = Http.request({
hostname: 'localhost',
port: server.info.port,
method: 'GET',
headers: { 'content-length': 42 }
});
req.on('error', Hoek.ignore);
req.flushHeaders();
await ready;
req.destroy();
const [request] = await log;
expect(request.response.output.statusCode).to.equal(499);
await server.stop({ timeout: 1 });
});
it('returns an unlogged bad request error when parser fails before request is setup', async () => {
const server = Hapi.server({ routes: { timeout: { server: false } } });
await server.start();
let responseCount = 0;
server.events.on('response', () => {
responseCount += 1;
});
const client = Net.connect(server.info.port);
const clientEnded = new Promise((resolve, reject) => {
let response = '';
client.on('data', (chunk) => {
response = response + chunk.toString();
});
client.on('end', () => resolve(response));
client.on('error', reject);
});
await new Promise((resolve) => client.on('connect', resolve));
client.write('hello\n\r');
const clientResponse = await clientEnded;
expect(clientResponse).to.contain('400 Bad Request');
expect(responseCount).to.equal(0);
await server.stop({ timeout: 1 });
});
it('returns normal response when parser fails with bad method after request is setup', async () => {
const server = Hapi.server({ routes: { timeout: { server: false } } });
server.route({ path: '/', method: 'GET', handler: () => 'PAYLOAD' });
await server.start();
const log = server.events.once('response');
const client = Net.connect(server.info.port);
const clientEnded = Wreck.read(client);
await new Promise((resolve) => client.on('connect', resolve));
client.write('GET / HTTP/1.1\r\nHost: test\r\nContent-Length: 0\r\n\r\ninvalid data');
const [request] = await log;
expect(request.response.statusCode).to.equal(200);
expect(request.response.source).to.equal('PAYLOAD');
const clientResponse = (await clientEnded).toString();
expect(clientResponse).to.contain('HTTP/1.1 200 OK');
const nextResponse = clientResponse.slice(clientResponse.indexOf('PAYLOAD') + 7);
expect(nextResponse).to.startWith('HTTP/1.1 400 Bad Request');
await server.stop({ timeout: 1 });
});
it('returns nothing when parser fails with bad method after request is setup and the connection is closed', async () => {
const server = Hapi.server({ routes: { timeout: { server: false } } });
server.route({ path: '/', method: 'GET', handler: (request, h) => {
request.raw.res.destroy();
return h.abandon;
} });
await server.start();
const log = server.events.once('response');
const client = Net.connect(server.info.port);
const clientEnded = Wreck.read(client);
await new Promise((resolve) => client.on('connect', resolve));
client.write('GET / HTTP/1.1\r\nHost: test\r\nContent-Length: 0\r\n\r\n\r\ninvalid data');
const [request] = await log;
expect(request.response.statusCode).to.be.undefined();
const clientResponse = (await clientEnded).toString();
expect(clientResponse).to.equal('');
await server.stop({ timeout: 1 });
});
it('returns a bad request when parser fails after request is setup (cleanStop false)', async () => {
const server = Hapi.server({ routes: { timeout: { server: false } }, operations: { cleanStop: false } });
server.route({ path: '/', method: 'GET', handler: Hoek.block });
await server.start();
const client = Net.connect(server.info.port);
const clientEnded = new Promise((resolve, reject) => {
let response = '';
client.on('data', (chunk) => {
response = response + chunk.toString();
});
client.on('end', () => resolve(response));
client.on('error', reject);
});
await new Promise((resolve) => client.on('connect', resolve));
client.write('GET / HTTP/1.1\r\nHost: test\nContent-Length: 0\r\n\r\ninvalid data');
const clientResponse = await clientEnded;
expect(clientResponse).to.contain('400 Bad Request');
await server.stop({ timeout: 1 });
});
it('returns a bad request for POST request when chunked parsing fails', async () => {
const server = Hapi.server({ routes: { timeout: { server: false } } });
server.route({ path: '/', method: 'POST', handler: () => 'ok', options: { payload: { parse: true } } });
await server.start();
const log = server.events.once('response');
const client = Net.connect(server.info.port);
const clientEnded = Wreck.read(client);
await new Promise((resolve) => client.on('connect', resolve));
client.write('POST / HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n');
await Hoek.wait(10);
client.write('not chunked\r\n');
const [request] = await log;
expect(request.response.statusCode).to.equal(400);
expect(request.response.source).to.contain({ error: 'Bad Request' });
const clientResponse = (await clientEnded).toString();
expect(clientResponse).to.contain('400 Bad Request');
await server.stop({ timeout: 1 });
});
it('returns a bad request for POST request when chunked parsing fails (cleanStop false)', async () => {
const server = Hapi.server({ routes: { timeout: { server: false } }, operations: { cleanStop: false } });
server.route({ path: '/', method: 'POST', handler: () => 'ok', options: { payload: { parse: true } } });
await server.start();
const client = Net.connect(server.info.port);
const clientEnded = Wreck.read(client);
await new Promise((resolve) => client.on('connect', resolve));
client.write('POST / HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n');
await Hoek.wait(10);
client.write('not chunked\r\n');
const clientResponse = (await clientEnded).toString();
expect(clientResponse).to.contain('400 Bad Request');
await server.stop({ timeout: 1 });
});
it('returns a bad request for POST request when chunked parsing fails', async () => {
const server = Hapi.server({ routes: { timeout: { server: false } } });
server.route({ path: '/', method: 'POST', handler: () => 'ok', options: { payload: { parse: true } } });
await server.start();
const log = server.events.once('response');
const client = Net.connect(server.info.port);
const clientEnded = Wreck.read(client);
await new Promise((resolve) => client.on('connect', resolve));
client.write('POST / HTTP/1.1\r\nHost: test\r\nContent-Length: 5\r\n\r\n');
await Hoek.wait(10);
client.write('111A1'); // Doesn't work if 'A' is replaced with '1' !?!
client.write('\Q\r\n'); // Extra bytes considered to be start of next request
client.end();
const [request] = await log;
expect(request.response.statusCode).to.equal(400);
expect(request.response.source).to.contain({ error: 'Bad Request' });
const clientResponse = (await clientEnded).toString();
expect(clientResponse).to.contain('400 Bad Request');
await server.stop({ timeout: 1 });
});
it('does not return an error when server is responding when the timeout occurs', async () => {
let ended = false;
const TestStream = class extends Stream.Readable {
_read(size) {
if (this.isDone) {
return;
}
this.isDone = true;
this.push('Hello');
setTimeout(() => {
this.push(null);
ended = true;
}, 150);
}
};
const handler = (request) => {
return new TestStream();
};
const timer = new Hoek.Bench();
const server = Hapi.server({ routes: { timeout: { server: 100 } } });
server.route({ method: 'GET', path: '/', handler });
await server.start();
const { res } = await Wreck.get('http://localhost:' + server.info.port);
expect(ended).to.be.true();
expect(timer.elapsed()).to.be.at.least(150);
expect(res.statusCode).to.equal(200);
await server.stop({ timeout: 1 });
});
it('does not return an error response when server is slower than timeout but response has started', async () => {
const streamHandler = (request) => {
const TestStream = class extends Stream.Readable {
_read(size) {
if (this.isDone) {
return;
}
this.isDone = true;
setTimeout(() => {
this.push('Hello');
}, 30);
setTimeout(() => {
this.push(null);
}, 60);
}
};
return new TestStream();
};
const server = Hapi.server({ routes: { timeout: { server: 50 } } });
server.route({ method: 'GET', path: '/stream', options: { handler: streamHandler } });
await server.start();
const { res } = await Wreck.get(`http://localhost:${server.info.port}/stream`);
expect(res.statusCode).to.equal(200);
await server.stop({ timeout: 1 });
});
it('does not return an error response when server takes less than timeout to respond', async () => {
const server = Hapi.server({ routes: { timeout: { server: 50 } } });
server.route({ method: 'GET', path: '/fast', handler: () => 'Fast' });
const res = await server.inject('/fast');
expect(res.statusCode).to.equal(200);
});
it('handles race condition between equal client and server timeouts', async (flags) => {
const onCleanup = [];
flags.onCleanup = async () => {
for (const cleanup of onCleanup) {
await cleanup();
}
};
const server = Hapi.server({ routes: { timeout: { server: 100 }, payload: { timeout: 100 } } });
server.route({ method: 'POST', path: '/timeout', options: { handler: Hoek.block } });
await server.start();
onCleanup.unshift(() => server.stop());
const timer = new Hoek.Bench();
const options = {
hostname: 'localhost',
port: server.info.port,
path: '/timeout',
method: 'POST'
};
const req = Http.request(options);
onCleanup.unshift(() => req.destroy());
req.write('\n');
const [res] = await Events.once(req, 'response');
expect([503, 408]).to.contain(res.statusCode);
expect(timer.elapsed()).to.be.at.least(80);
await Events.once(req, 'close'); // Ensures that req closes without error
});
});
describe('event()', () => {
it('does not emit request error on normal close', async () => {
const server = Hapi.server();
const events = [];
server.events.on('request', (request, event, tags) => events.push(tags));
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
await server.start();
const { payload } = await Wreck.get('http://localhost:' + server.info.port);
expect(payload.toString()).to.equal('ok');
await server.stop();
expect(events).to.have.length(0);
});
});
});
================================================
FILE: test/response.js
================================================
'use strict';
const Events = require('events');
const Http = require('http');
const Path = require('path');
const Stream = require('stream');
const Code = require('@hapi/code');
const Handlebars = require('handlebars');
const LegacyReadableStream = require('legacy-readable-stream');
const Hapi = require('..');
const Inert = require('@hapi/inert');
const Lab = require('@hapi/lab');
const Vision = require('@hapi/vision');
const Response = require('../lib/response');
const internals = {};
const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;
describe('Response', () => {
it('returns a response', async () => {
const handler = (request, h) => {
return h.response('text')
.type('text/plain')
.charset('ISO-8859-1')
.ttl(1000)
.header('set-cookie', 'abc=123')
.state('sid', 'abcdefg123456')
.state('other', 'something', { isSecure: true })
.unstate('x')
.header('Content-Type', 'text/plain; something=something')
.header('vary', 'x-control')
.header('combo', 'o')
.header('combo', 'k', { append: true, separator: '-' })
.header('combo', 'bad', { override: false })
.code(200)
.message('Super');
};
const server = Hapi.server({ compression: { minBytes: 1 } });
server.route({ method: 'GET', path: '/', options: { handler, cache: { expiresIn: 9999 } } });
server.state('sid', { encoding: 'base64' });
server.state('always', { autoValue: 'present' });
const postHandler = (request, h) => {
h.state('test', '123');
h.unstate('empty', { path: '/path' });
return h.continue;
};
server.ext('onPostHandler', postHandler);
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result).to.exist();
expect(res.result).to.equal('text');
expect(res.statusMessage).to.equal('Super');
expect(res.headers['cache-control']).to.equal('max-age=1, must-revalidate, private');
expect(res.headers['content-type']).to.equal('text/plain; something=something; charset=ISO-8859-1');
expect(res.headers['set-cookie']).to.equal(['abc=123', 'sid=YWJjZGVmZzEyMzQ1Ng==; Secure; HttpOnly; SameSite=Strict', 'other=something; Secure; HttpOnly; SameSite=Strict', 'x=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Strict', 'test=123; Secure; HttpOnly; SameSite=Strict', 'empty=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Strict; Path=/path', 'always=present; Secure; HttpOnly; SameSite=Strict']);
expect(res.headers.vary).to.equal('x-control,accept-encoding');
expect(res.headers.combo).to.equal('o-k');
});
it('sets content-type charset (trailing semi column)', async () => {
const handler = (request, h) => {
return h.response('text').header('Content-Type', 'text/plain; something=something;');
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers['content-type']).to.equal('text/plain; something=something; charset=utf-8');
});
describe('_setSource()', () => {
it('returns an empty string response', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
handler: () => ''
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
expect(res.headers['content-length']).to.not.exist();
expect(res.headers['content-type']).to.equal('text/html; charset=utf-8');
expect(res.result).to.equal(null);
expect(res.payload).to.equal('');
});
it('returns a null response', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
handler: () => null
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
expect(res.headers['content-length']).to.not.exist();
expect(res.headers['content-type']).to.not.exist();
expect(res.result).to.equal(null);
expect(res.payload).to.equal('');
});
it('returns a stream', async () => {
const handler = (request) => {
const stream = new Stream.Readable({
read() {
this.push('x');
this.push(null);
}
});
return stream;
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('x');
expect(res.statusCode).to.equal(200);
expect(res.headers['content-type']).to.equal('application/octet-stream');
});
});
describe('code()', () => {
it('sets manual code regardless of emptyStatusCode override', async () => {
const server = Hapi.server({ routes: { response: { emptyStatusCode: 200 } } });
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response().code(204) });
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
});
});
describe('header()', () => {
it('appends to set-cookie header', async () => {
const handler = (request, h) => {
return h.response('ok').header('set-cookie', 'A').header('set-cookie', 'B', { append: true });
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers['set-cookie']).to.equal(['A', 'B']);
});
it('sets null header', async () => {
const handler = (request, h) => {
return h.response('ok').header('set-cookie', null);
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers['set-cookie']).to.not.exist();
});
it('throws error on non-ascii value', async () => {
const handler = (request, h) => {
return h.response('ok').header('set-cookie', decodeURIComponent('%E0%B4%8Aset-cookie:%20foo=bar'));
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
it('throws error on non-ascii value (header name)', async () => {
const handler = (request, h) => {
const badName = decodeURIComponent('%E0%B4%8Aset-cookie:%20foo=bar');
return h.response('ok').header(badName, 'value');
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
it('throws error on non-ascii value (buffer)', async () => {
const handler = (request, h) => {
return h.response('ok').header('set-cookie', Buffer.from(decodeURIComponent('%E0%B4%8Aset-cookie:%20foo=bar')));
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
});
describe('created()', () => {
it('returns a response (created)', async () => {
const handler = (request, h) => {
return h.response({ a: 1 }).created('/special');
};
const server = Hapi.server();
server.route({ method: 'POST', path: '/', handler });
const res = await server.inject({ method: 'POST', url: '/' });
expect(res.result).to.equal({ a: 1 });
expect(res.statusCode).to.equal(201);
expect(res.headers.location).to.equal('/special');
expect(res.headers['cache-control']).to.equal('no-cache');
});
it('returns error on created with GET', async () => {
const handler = (request, h) => {
return h.response().created('/something');
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
it('does not return an error on created with PUT', async () => {
const handler = (request, h) => {
return h.response({ a: 1 }).created();
};
const server = Hapi.server();
server.route({ method: 'PUT', path: '/', handler });
const res = await server.inject({ method: 'PUT', url: '/' });
expect(res.result).to.equal({ a: 1 });
expect(res.statusCode).to.equal(201);
});
it('does not return an error on created with PATCH', async () => {
const handler = (request, h) => {
return h.response({ a: 1 }).created();
};
const server = Hapi.server();
server.route({ method: 'PATCH', path: '/', handler });
const res = await server.inject({ method: 'PATCH', url: '/' });
expect(res.result).to.equal({ a: 1 });
expect(res.statusCode).to.equal(201);
});
});
describe('state()', () => {
it('returns an error on bad cookie', async () => {
const handler = (request, h) => {
return h.response('text').state(';sid', 'abcdefg123456');
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.exist();
expect(res.statusCode).to.equal(500);
expect(res.result.message).to.equal('An internal server error occurred');
expect(res.headers['set-cookie']).to.not.exist();
});
});
describe('unstate()', () => {
it('allows options', async () => {
const handler = (request, h) => {
return h.response().unstate('session', { path: '/unset', isSecure: true });
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
expect(res.headers['set-cookie']).to.equal(['session=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Strict; Path=/unset']);
});
});
describe('vary()', () => {
it('sets Vary header with single value', async () => {
const handler = (request, h) => {
return h.response('ok').vary('x');
};
const server = Hapi.server({ compression: { minBytes: 1 } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('ok');
expect(res.statusCode).to.equal(200);
expect(res.headers.vary).to.equal('x,accept-encoding');
});
it('sets Vary header with multiple values', async () => {
const handler = (request, h) => {
return h.response('ok').vary('x').vary('y');
};
const server = Hapi.server({ compression: { minBytes: 1 } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('ok');
expect(res.statusCode).to.equal(200);
expect(res.headers.vary).to.equal('x,y,accept-encoding');
});
it('sets Vary header with *', async () => {
const handler = (request, h) => {
return h.response('ok').vary('*');
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('ok');
expect(res.statusCode).to.equal(200);
expect(res.headers.vary).to.equal('*');
});
it('leaves Vary header with * on additional values', async () => {
const handler = (request, h) => {
return h.response('ok').vary('*').vary('x');
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('ok');
expect(res.statusCode).to.equal(200);
expect(res.headers.vary).to.equal('*');
});
it('drops other Vary header values when set to *', async () => {
const handler = (request, h) => {
return h.response('ok').vary('x').vary('*');
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('ok');
expect(res.statusCode).to.equal(200);
expect(res.headers.vary).to.equal('*');
});
it('sets Vary header with multiple similar and identical values', async () => {
const handler = (request, h) => {
return h.response('ok').vary('x').vary('xyz').vary('xy').vary('x');
};
const server = Hapi.server({ compression: { minBytes: 1 } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('ok');
expect(res.statusCode).to.equal(200);
expect(res.headers.vary).to.equal('x,xyz,xy,accept-encoding');
});
});
describe('etag()', () => {
it('sets etag', async () => {
const handler = (request, h) => {
return h.response('ok').etag('abc');
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers.etag).to.equal('"abc"');
});
it('sets weak etag', async () => {
const handler = (request, h) => {
return h.response('ok').etag('abc', { weak: true });
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers.etag).to.equal('W/"abc"');
});
it('ignores varyEtag when etag header is removed', async () => {
const handler = (request, h) => {
const response = h.response('ok').etag('abc').vary('x');
delete response.headers.etag;
return response;
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers.etag).to.not.exist();
});
it('leaves etag header when varyEtag is false', async () => {
const handler = (request, h) => {
return h.response('ok').etag('abc', { vary: false }).vary('x');
};
const server = Hapi.server({ compression: { minBytes: 1 } });
server.route({ method: 'GET', path: '/', handler });
const res1 = await server.inject('/');
expect(res1.statusCode).to.equal(200);
expect(res1.headers.etag).to.equal('"abc"');
const res2 = await server.inject({ url: '/', headers: { 'if-none-match': '"abc-gzip"', 'accept-encoding': 'gzip' } });
expect(res2.statusCode).to.equal(200);
expect(res2.headers.etag).to.equal('"abc"');
});
it('applies varyEtag when returning 304 due to if-modified-since match', async () => {
const mdate = new Date().toUTCString();
const handler = (request, h) => {
return h.response('ok').etag('abc').header('last-modified', mdate);
};
const server = Hapi.server({ compression: { minBytes: 1 } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject({ url: '/', headers: { 'if-modified-since': mdate, 'accept-encoding': 'gzip' } });
expect(res.statusCode).to.equal(304);
expect(res.headers.etag).to.equal('"abc-gzip"');
});
});
describe('passThrough()', () => {
it('passes stream headers and code through', async () => {
const TestStream = class extends Stream.Readable {
constructor() {
super();
this.statusCode = 299;
this.headers = { xcustom: 'some value', 'content-type': 'something/special' };
}
_read(size) {
if (this.isDone) {
return;
}
this.isDone = true;
this.push('x');
this.push(null);
}
};
const handler = (request) => {
return new TestStream();
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('x');
expect(res.statusCode).to.equal(299);
expect(res.headers.xcustom).to.equal('some value');
expect(res.headers['content-type']).to.equal('something/special');
});
it('excludes connection header and connection options', async () => {
const upstreamConnectionHeader = 'x-test, x-test-also';
const TestStream = class extends Stream.Readable {
constructor() {
super();
this.statusCode = 200;
this.headers = {
connection: upstreamConnectionHeader,
'x-test': 'something',
'x-test-also': 'also'
};
}
_read(size) {
if (this.isDone) {
return;
}
this.isDone = true;
this.push('x');
this.push(null);
}
};
const handler = (request) => {
return new TestStream();
};
const server = new Hapi.Server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('x');
expect(res.statusCode).to.equal(200);
expect(res.headers.connection).to.not.equal(upstreamConnectionHeader);
expect(res.headers['x-test']).to.not.exist();
expect(res.headers['x-test-also']).to.not.exist();
});
it('excludes stream headers and code when passThrough is false', async () => {
const TestStream = class extends Stream.Readable {
constructor() {
super();
this.statusCode = 299;
this.headers = { xcustom: 'some value' };
}
_read(size) {
if (this.isDone) {
return;
}
this.isDone = true;
this.push('x');
this.push(null);
}
};
const handler = (request, h) => {
return h.response(new TestStream()).passThrough(false);
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('x');
expect(res.statusCode).to.equal(200);
expect(res.headers.xcustom).to.not.exist();
});
it('ignores stream headers when empty', async () => {
const TestStream = class extends Stream.Readable {
constructor() {
super();
this.statusCode = 299;
this.headers = {};
}
_read(size) {
if (this.isDone) {
return;
}
this.isDone = true;
this.push('x');
this.push(null);
}
};
const handler = (request) => {
return new TestStream();
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('x');
expect(res.statusCode).to.equal(299);
expect(res.headers.xcustom).to.not.exist();
});
it('retains local headers with stream headers pass-through', async () => {
const TestStream = class extends Stream.Readable {
constructor() {
super();
this.headers = { xcustom: 'some value', 'set-cookie': 'a=1' };
}
_read(size) {
if (this.isDone) {
return;
}
this.isDone = true;
this.push('x');
this.push(null);
}
};
const handler = (request, h) => {
return h.response(new TestStream()).header('xcustom', 'other value').state('b', '2');
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('x');
expect(res.headers.xcustom).to.equal('other value');
expect(res.headers['set-cookie']).to.equal(['a=1', 'b=2; Secure; HttpOnly; SameSite=Strict']);
});
});
describe('replacer()', () => {
it('errors when called on wrong type', async () => {
const handler = (request, h) => {
return h.response('x').replacer(['x']);
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
});
describe('compressed()', () => {
it('errors on missing encoding', async () => {
const handler = (request, h) => {
return h.response('x').compressed();
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
it('errors on invalid encoding', async () => {
const handler = (request, h) => {
return h.response('x').compressed(123);
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
});
describe('spaces()', () => {
it('errors when called on wrong type', async () => {
const handler = (request, h) => {
return h.response('x').spaces(2);
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
});
describe('suffix()', () => {
it('errors when called on wrong type', async () => {
const handler = (request, h) => {
return h.response('x').suffix('x');
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
});
describe('escape()', () => {
it('returns 200 when called with true', async () => {
const handler = (request, h) => {
return h.response({ x: 'x' }).escape(true);
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
});
it('errors when called on wrong type', async () => {
const handler = (request, h) => {
return h.response('x').escape('x');
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
});
describe('type()', () => {
it('returns a file in the response with the correct headers using custom mime type', async () => {
const server = Hapi.server({ routes: { files: { relativeTo: Path.join(__dirname, '../') } } });
await server.register(Inert);
const handler = (request, h) => {
return h.file('./LICENSE.md').type('application/example');
};
server.route({ method: 'GET', path: '/file', handler });
const res = await server.inject('/file');
expect(res.headers['content-type']).to.equal('application/example');
});
});
describe('charset()', () => {
it('sets charset with default type', async () => {
const handler = (request, h) => {
return h.response('text').charset('abc');
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers['content-type']).to.equal('text/html; charset=abc');
});
it('sets charset with default type in onPreResponse', async () => {
const onPreResponse = (request, h) => {
request.response.charset('abc');
return h.continue;
};
const server = Hapi.server();
server.ext('onPreResponse', onPreResponse);
server.route({ method: 'GET', path: '/', handler: () => 'text' });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers['content-type']).to.equal('text/html; charset=abc');
});
it('sets type inside marshal', async () => {
const handler = (request) => {
const marshal = (response) => {
if (!response.headers['content-type']) {
response.type('text/html');
}
return response.source.value;
};
return request.generateResponse({ value: 'text' }, { variety: 'test', marshal });
};
const onPreResponse = (request, h) => {
request.response.charset('abc');
return h.continue;
};
const server = Hapi.server();
server.ext('onPreResponse', onPreResponse);
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.headers['content-type']).to.equal('text/html; charset=abc');
});
});
describe('redirect()', () => {
it('returns a redirection response', async () => {
const handler = (request, h) => {
return h.response('Please wait while we send your elsewhere').redirect('/example');
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('http://example.org/');
expect(res.result).to.exist();
expect(res.headers.location).to.equal('/example');
expect(res.statusCode).to.equal(302);
});
it('returns a redirection response using verbose call', async () => {
const handler = (request, h) => {
return h.response('We moved!').redirect().location('/examplex');
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.exist();
expect(res.result).to.equal('We moved!');
expect(res.headers.location).to.equal('/examplex');
expect(res.statusCode).to.equal(302);
});
it('returns a 301 redirection response', async () => {
const handler = (request, h) => {
return h.response().redirect('example').permanent().rewritable();
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(301);
});
it('returns a 302 redirection response', async () => {
const handler = (request, h) => {
return h.response().redirect('example').temporary().rewritable();
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(302);
});
it('returns a 307 redirection response', async () => {
const handler = (request, h) => {
return h.response().redirect('example').temporary().rewritable(false);
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(307);
});
it('returns a 308 redirection response', async () => {
const handler = (request, h) => {
return h.response().redirect('example').permanent().rewritable(false);
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(308);
});
it('returns a 301 redirection response (reversed methods)', async () => {
const handler = (request, h) => {
return h.response().redirect('example').rewritable().permanent();
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(301);
});
it('returns a 302 redirection response (reversed methods)', async () => {
const handler = (request, h) => {
return h.response().redirect('example').rewritable().temporary();
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(302);
});
it('returns a 307 redirection response (reversed methods)', async () => {
const handler = (request, h) => {
return h.response().redirect('example').rewritable(false).temporary();
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(307);
});
it('returns a 308 redirection response (reversed methods)', async () => {
const handler = (request, h) => {
return h.response().redirect('example').rewritable(false).permanent();
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(308);
});
it('returns a 302 redirection response (flip flop)', async () => {
const handler = (request, h) => {
return h.response().redirect('example').permanent().temporary();
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(302);
});
});
describe('_marshal()', () => {
it('emits request-error when view file for handler not found', async () => {
const server = Hapi.server({ debug: false });
await server.register(Vision);
server.views({
engines: { 'html': Handlebars },
path: __dirname
});
const log = server.events.once({ name: 'request', channels: 'error' });
server.route({ method: 'GET', path: '/{param}', handler: { view: 'templates/invalid' } });
const res = await server.inject('/hello');
expect(res.statusCode).to.equal(500);
expect(res.result).to.exist();
expect(res.result.message).to.equal('An internal server error occurred');
const [, event] = await log;
expect(event.error.message).to.contain('The partial x could not be found: The partial x could not be found');
});
it('returns a formatted response (spaces)', async () => {
const handler = (request) => {
return { a: 1, b: 2, '<': '&' };
};
const server = Hapi.server({ routes: { json: { space: 4, suffix: '\n', escape: true } } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.payload).to.equal('{\n \"a\": 1,\n \"b\": 2,\n \"\\u003c\": \"\\u0026\"\n}\n');
});
it('returns a formatted response (replacer and spaces', async () => {
const handler = (request) => {
return { a: 1, b: 2, '<': '&' };
};
const server = Hapi.server({ routes: { json: { replacer: ['a', '<'], space: 4, suffix: '\n', escape: true } } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.payload).to.equal('{\n \"a\": 1,\n \"\\u003c\": \"\\u0026\"\n}\n');
});
it('returns a response with options', async () => {
const handler = (request, h) => {
return h.response({ a: 1, b: 2, '<': '&' }).type('application/x-test').spaces(2).replacer(['a']).suffix('\n').escape(false);
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.payload).to.equal('{\n \"a\": 1\n}\n');
expect(res.headers['content-type']).to.equal('application/x-test');
});
it('returns a response with options (different order)', async () => {
const handler = (request, h) => {
return h.response({ a: 1, b: 2, '<': '&' }).type('application/x-test').escape(false).replacer(['a']).suffix('\n').spaces(2);
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.payload).to.equal('{\n \"a\": 1\n}\n');
expect(res.headers['content-type']).to.equal('application/x-test');
});
it('captures object which cannot be stringify', async () => {
const handler = (request) => {
const obj = {};
obj.a = obj;
return obj;
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
it('errors on non-readable stream response', async () => {
const streamHandler = (request, h) => {
const stream = new Stream();
stream.writable = true;
return h.response(stream);
};
const writableHandler = (request, h) => {
const writable = new Stream.Writable();
writable._write = function () { };
return h.response(writable);
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/stream', handler: streamHandler });
server.route({ method: 'GET', path: '/writable', handler: writableHandler });
await server.initialize();
const log1 = server.events.once({ name: 'request', channels: 'error' });
const res1 = await server.inject('/stream');
expect(res1.statusCode).to.equal(500);
const [, event1] = await log1;
expect(event1.error).to.be.an.error('Cannot reply with a stream-like object that is not an instance of Stream.Readable');
const log2 = server.events.once({ name: 'request', channels: 'error' });
const res2 = await server.inject('/writable');
expect(res2.statusCode).to.equal(500);
const [, event2] = await log2;
expect(event2.error).to.be.an.error('Cannot reply with a stream-like object that is not an instance of Stream.Readable');
});
it('errors on an http client stream response', async () => {
const streamHandler = (request, h) => {
const req = Http.get(request.server.info.uri);
req.abort();
return h.response(req);
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/stream', handler: streamHandler });
const log = server.events.once({ name: 'request', channels: 'error' });
await server.initialize();
const res = await server.inject('/stream');
expect(res.statusCode).to.equal(500);
const [, event] = await log;
expect(event.error).to.be.an.error('Cannot reply with a stream-like object that is not an instance of Stream.Readable');
});
it('errors on a legacy readable stream response', async () => {
const streamHandler = () => {
const stream = new LegacyReadableStream.Readable();
stream._read = function (size) {
const chunk = new Array(size).join('x');
setTimeout(() => {
this.push(chunk);
}, 10);
};
return stream;
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/stream', handler: streamHandler });
const log = server.events.once({ name: 'request', channels: 'error' });
await server.initialize();
const res = await server.inject('/stream');
expect(res.statusCode).to.equal(500);
const [, event] = await log;
expect(event.error).to.be.an.error('Cannot reply with a stream-like object that is not an instance of Stream.Readable');
});
it('errors on objectMode stream response', async () => {
const TestStream = class extends Stream.Readable {
constructor() {
super({ objectMode: true });
}
_read(size) {
if (this.isDone) {
return;
}
this.isDone = true;
this.push({ x: 1 });
this.push({ y: 1 });
this.push(null);
}
};
const handler = (request, h) => {
return h.response(new TestStream());
};
const server = Hapi.server({ debug: false });
server.route({ method: 'GET', path: '/', handler });
const log = server.events.once({ name: 'request', channels: 'error' });
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
const [, event] = await log;
expect(event.error).to.be.an.error('Cannot reply with stream in object mode');
});
});
describe('_prepare()', () => {
it('boomifies response prepare error', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
handler: (request) => {
const prepare = () => {
throw new Error('boom');
};
return request.generateResponse('nothing', { variety: 'special', marshal: null, prepare, close: null });
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(500);
});
it('is only called once for returned responses', async () => {
let calls = 0;
const pre = (request, h) => {
const prepare = (response) => {
++calls;
return response;
};
return request.generateResponse(null, { prepare });
};
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
options: {
pre: [
{ method: pre, assign: 'p' }
],
handler: (request) => request.preResponses.p
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
expect(calls).to.equal(1);
});
});
describe('_tap()', () => {
it('peeks into the response stream', async () => {
const server = Hapi.server();
let output = '';
server.route({
method: 'GET',
path: '/',
handler: (request, h) => {
const response = h.response('1234567890');
response.events.on('peek', (chunk, encoding) => {
output += chunk.toString();
});
response.events.once('finish', () => {
output += '!';
});
return response;
}
});
await server.inject('/');
expect(output).to.equal('1234567890!');
});
it('peeks into the response stream (finish only)', async () => {
const server = Hapi.server();
let output = false;
server.route({
method: 'GET',
path: '/',
handler: (request, h) => {
const response = h.response('1234567890');
response.events.once('finish', () => {
output = true;
});
return response;
}
});
await server.inject('/');
expect(output).to.be.true();
});
it('peeks into the response stream (empty)', async () => {
const server = Hapi.server();
let output = '';
server.route({
method: 'GET',
path: '/',
handler: (request, h) => {
const response = h.response(null);
response.events.on('peek', (chunk, encoding) => { });
response.events.once('finish', () => {
output += '!';
});
return response;
}
});
await server.inject('/');
expect(output).to.equal('!');
});
it('peeks into the response stream (empty 304)', async () => {
const server = Hapi.server();
let output = '';
server.route({
method: 'GET',
path: '/',
handler: (request, h) => {
const response = h.response(null).code(304);
response.events.on('peek', (chunk, encoding) => { });
response.events.once('finish', () => {
output += '!';
});
return response;
}
});
await server.inject('/');
expect(output).to.equal('!');
});
});
describe('_close()', () => {
it('calls custom close processor', async () => {
let closed = false;
const close = function (response) {
closed = true;
};
const handler = (request) => {
return request.generateResponse(null, { close });
};
const server = Hapi.server();
server.route({ method: 'GET', path: '/', handler });
await server.inject('/');
expect(closed).to.be.true();
});
it('logs custom close processor error', async () => {
const close = function (response) {
throw new Error('oops');
};
const handler = (request) => {
return request.generateResponse(null, { close });
};
const server = Hapi.server();
const log = server.events.once('request');
server.route({ method: 'GET', path: '/', handler });
await server.inject('/');
const [, event] = await log;
expect(event.tags).to.equal(['response', 'cleanup', 'error']);
expect(event.error).to.be.an.error('oops');
});
});
describe('Peek', () => {
it('taps into pass-through stream', async () => {
// Source
const Source = class extends Stream.Readable {
constructor(values) {
super();
this.data = values;
this.pos = 0;
}
_read(/* size */) {
if (this.pos === this.data.length) {
this.push(null);
return;
}
this.push(this.data[this.pos++]);
}
};
// Target
const Target = class extends Stream.Writable {
constructor() {
super();
this.data = [];
}
_write(chunk, encoding, callback) {
this.data.push(chunk.toString());
return callback();
}
};
// Peek
const emitter = new Events.EventEmitter();
const peek = new Response.Peek(emitter);
const chunks = ['abcd', 'efgh', 'ijkl', 'mnop', 'qrst', 'uvwx'];
const source = new Source(chunks);
const target = new Target();
const seen = [];
emitter.on('peek', (update) => {
const chunk = update[0];
seen.push(chunk.toString());
});
const finish = new Promise((resolve) => {
emitter.once('finish', () => {
expect(seen).to.equal(chunks);
expect(target.data).to.equal(chunks);
resolve();
});
});
source.pipe(peek).pipe(target);
await finish;
});
});
});
================================================
FILE: test/route.js
================================================
'use strict';
const Path = require('path');
const Code = require('@hapi/code');
const Hapi = require('..');
const Inert = require('@hapi/inert');
const Joi = require('joi');
const Lab = require('@hapi/lab');
const Subtext = require('@hapi/subtext');
const internals = {};
const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;
describe('Route', () => {
it('registers with options function', async () => {
const server = Hapi.server();
server.bind({ a: 1 });
server.app.b = 2;
server.route({
method: 'GET',
path: '/',
options: function (srv) {
const a = this.a;
return {
handler: () => a + srv.app.b
};
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal(3);
});
it('registers with config', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
config: {
handler: () => 'ok'
}
});
const res = await server.inject('/');
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('ok');
});
it('throws an error when a route is missing a path', () => {
expect(() => {
const server = Hapi.server();
server.route({ method: 'GET', handler: () => null });
}).to.throw(/"path" is required/);
});
it('throws an error when a route is missing a method', () => {
expect(() => {
const server = Hapi.server();
server.route({ path: '/', handler: () => null });
}).to.throw(/"method" is required/);
});
it('throws an error when a route has a malformed method name', () => {
expect(() => {
const server = Hapi.server();
server.route({ method: '"GET"', path: '/', handler: () => null });
}).to.throw(/Invalid route options/);
});
it('throws an error when a route uses the HEAD method', () => {
expect(() => {
const server = Hapi.server();
server.route({ method: 'HEAD', path: '/', handler: () => null });
}).to.throw('Cannot set HEAD route: /');
});
it('throws an error when a route is missing a handler', () => {
expect(() => {
const server = Hapi.server();
server.route({ path: '/test', method: 'put' });
}).to.throw('Missing or undefined handler: PUT /test');
});
it('throws when handler is missing in config', () => {
const server = Hapi.server();
expect(() => {
server.route({ method: 'GET', path: '/', options: {} });
}).to.throw('Missing or undefined handler: GET /');
});
it('throws when path has trailing slash and server set to strip', () => {
const server = Hapi.server({ router: { stripTrailingSlash: true } });
expect(() => {
server.route({ method: 'GET', path: '/test/', handler: () => null });
}).to.throw('Path cannot end with a trailing slash when configured to strip: GET /test/');
});
it('allows / when path has trailing slash and server set to strip', () => {
const server = Hapi.server({ router: { stripTrailingSlash: true } });
expect(() => {
server.route({ method: 'GET', path: '/', handler: () => null });
}).to.not.throw();
});
it('sets route plugins and app settings', async () => {
const handler = (request) => (request.route.settings.app.x + request.route.settings.plugins.x.y);
const server = Hapi.server();
server.route({ method: 'GET', path: '/', options: { handler, app: { x: 'o' }, plugins: { x: { y: 'k' } } } });
const res = await server.inject('/');
expect(res.result).to.equal('ok');
});
it('throws when validation is set without payload parsing', () => {
const server = Hapi.server();
expect(() => {
server.route({ method: 'POST', path: '/', handler: () => null, options: { validate: { payload: {}, validator: Joi }, payload: { parse: false } } });
}).to.throw('Route payload must be set to \'parse\' when payload validation enabled: POST /');
});
it('throws when validation is set without path parameters', () => {
const server = Hapi.server();
expect(() => {
server.route({ method: 'POST', path: '/', handler: () => null, options: { validate: { params: {} } } });
}).to.throw('Cannot set path parameters validations without path parameters: POST /');
});
it('ignores payload when overridden', async () => {
const server = Hapi.server();
server.route({
method: 'POST',
path: '/',
handler: (request) => request.payload
});
server.ext('onRequest', (request, h) => {
request.payload = 'x';
return h.continue;
});
const res = await server.inject({ method: 'POST', url: '/', payload: 'y' });
expect(res.statusCode).to.equal(200);
expect(res.result).to.equal('x');
});
it('ignores payload parsing errors', async () => {
const server = Hapi.server();
server.route({
method: 'POST',
path: '/',
handler: () => 'ok',
options: {
payload: {
parse: true,
failAction: 'ignore'
}
}
});
const res = await server.inject({ method: 'POST', url: '/', payload: '{a:"abc"}' });
expect(res.statusCode).to.equal(200);
});
it('logs payload parsing errors', async () => {
const server = Hapi.server();
server.route({
method: 'POST',
path: '/',
handler: () => 'ok',
options: {
payload: {
parse: true,
failAction: 'log'
}
}
});
let logged;
server.events.on({ name: 'request', channels: 'internal' }, (request, event, tags) => {
if (tags.payload && tags.error) {
logged = event;
}
});
const res = await server.inject({ method: 'POST', url: '/', payload: '{a:"abc"}' });
expect(res.statusCode).to.equal(200);
expect(logged).to.be.an.object();
expect(logged.error).to.be.an.error('Invalid request payload JSON format');
expect(logged.error.data).to.be.an.error(SyntaxError, /at position 1/);
});
it('returns payload parsing errors', async () => {
const server = Hapi.server();
server.route({
method: 'POST',
path: '/',
handler: () => 'ok',
options: {
payload: {
parse: true,
failAction: 'error'
}
}
});
const res = await server.inject({ method: 'POST', url: '/', payload: '{a:"abc"}' });
expect(res.statusCode).to.equal(400);
expect(res.result.message).to.equal('Invalid request payload JSON format');
});
it('replaces payload parsing errors with custom handler', async () => {
const server = Hapi.server();
server.route({
method: 'POST',
path: '/',
handler: () => 'ok',
options: {
payload: {
parse: true,
failAction: function (request, h, error) {
return h.response('This is a custom error').code(418).takeover();
}
}
}
});
const res = await server.inject({ method: 'POST', url: '/', payload: '{a:"abc"}' });
expect(res.statusCode).to.equal(418);
expect(res.result).to.equal('This is a custom error');
});
it('throws when validation is set on GET', () => {
const server = Hapi.server();
expect(() => {
server.route({ method: 'GET', path: '/', handler: () => null, options: { validate: { payload: {} } } });
}).to.throw('Cannot validate HEAD or GET request payload: GET /');
});
it('throws when payload parsing is set on GET', () => {
const server = Hapi.server();
expect(() => {
server.route({ method: 'GET', path: '/', handler: () => null, options: { payload: { parse: true } } });
}).to.throw('Cannot set payload settings on HEAD or GET request: GET /');
});
it('ignores validation on * route when request is GET', async () => {
const server = Hapi.server();
server.validator(Joi);
server.route({ method: '*', path: '/', handler: () => null, options: { validate: { payload: { a: Joi.required() } } } });
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
});
it('ignores validation on * route when request is HEAD', async () => {
const server = Hapi.server();
server.validator(Joi);
server.route({ method: '*', path: '/', handler: () => null, options: { validate: { payload: { a: Joi.required() } } } });
const res = await server.inject({ url: '/', method: 'HEAD' });
expect(res.statusCode).to.equal(204);
});
it('skips payload on * route when request is HEAD', async (flags) => {
const orig = Subtext.parse;
let called = false;
Subtext.parse = () => {
called = true;
};
flags.onCleanup = () => {
Subtext.parse = orig;
};
const server = Hapi.server();
server.route({ method: '*', path: '/', handler: () => null });
const res = await server.inject({ url: '/', method: 'HEAD' });
expect(res.statusCode).to.equal(204);
expect(called).to.be.false();
});
it('throws error when the default routes payload validation is set without payload parsing', () => {
expect(() => {
Hapi.server({ routes: { validate: { payload: {}, validator: Joi }, payload: { parse: false } } });
}).to.throw('Route payload must be set to \'parse\' when payload validation enabled');
});
it('throws error when the default routes state validation is set without state parsing', () => {
expect(() => {
Hapi.server({ routes: { validate: { state: {}, validator: Joi }, state: { parse: false } } });
}).to.throw('Route state must be set to \'parse\' when state validation enabled');
});
it('ignores default validation on GET', async () => {
const server = Hapi.server({ routes: { validate: { payload: { a: Joi.required() }, validator: Joi } } });
server.route({ method: 'GET', path: '/', handler: () => null });
const res = await server.inject('/');
expect(res.statusCode).to.equal(204);
});
it('shallow copies route config bind', async () => {
const server = Hapi.server();
const context = { key: 'is ' };
let count = 0;
Object.defineProperty(context, 'test', {
enumerable: true,
configurable: true,
get: function () {
++count;
}
});
const handler = function (request) {
return this.key + (this === context);
};
server.route({ method: 'GET', path: '/', handler, options: { bind: context } });
const res = await server.inject('/');
expect(res.result).to.equal('is true');
expect(count).to.equal(0);
});
it('shallow copies route config bind (server.bind())', async () => {
const server = Hapi.server();
const context = { key: 'is ' };
let count = 0;
Object.defineProperty(context, 'test', {
enumerable: true,
configurable: true,
get: function () {
++count;
}
});
const handler = function (request) {
return this.key + (this === context);
};
server.bind(context);
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('is true');
expect(count).to.equal(0);
});
it('shallow copies route config bind (connection defaults)', async () => {
const context = { key: 'is ' };
const server = Hapi.server({ routes: { bind: context } });
let count = 0;
Object.defineProperty(context, 'test', {
enumerable: true,
configurable: true,
get: function () {
++count;
}
});
const handler = function (request) {
return this.key + (this === context);
};
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('is true');
expect(count).to.equal(0);
});
it('shallow copies route config bind (server defaults)', async () => {
const context = { key: 'is ' };
let count = 0;
Object.defineProperty(context, 'test', {
enumerable: true,
configurable: true,
get: function () {
++count;
}
});
const handler = function (request) {
return this.key + (this === context);
};
const server = Hapi.server({ routes: { bind: context } });
server.route({ method: 'GET', path: '/', handler });
const res = await server.inject('/');
expect(res.result).to.equal('is true');
expect(count).to.equal(0);
});
it('overrides server relativeTo', async () => {
const server = Hapi.server();
await server.register(Inert);
const handler = (request, h) => h.file('./package.json');
server.route({ method: 'GET', path: '/file', handler, options: { files: { relativeTo: Path.join(__dirname, '../') } } });
const res = await server.inject('/file');
expect(res.payload).to.contain('hapi');
});
it('allows payload timeout more then socket timeout', () => {
expect(() => {
Hapi.server({ routes: { payload: { timeout: 60000 }, timeout: { socket: 12000 } } });
}).to.not.throw();
});
it('allows payload timeout more then socket timeout (node default)', () => {
expect(() => {
Hapi.server({ routes: { payload: { timeout: 6000000 } } });
}).to.not.throw();
});
it('allows server timeout more then socket timeout', () => {
expect(() => {
Hapi.server({ routes: { timeout: { server: 60000, socket: 12000 } } });
}).to.not.throw();
});
it('allows server timeout more then socket timeout (node default)', () => {
expect(() => {
Hapi.server({ routes: { timeout: { server: 6000000 } } });
}).to.not.throw();
});
it('ignores large server timeout when socket timeout disabled', () => {
expect(() => {
Hapi.server({ routes: { timeout: { server: 6000000, socket: false } } });
}).to.not.throw();
});
describe('extensions', () => {
it('combine connection extensions (route last)', async () => {
const server = Hapi.server();
const onRequest = (request, h) => {
request.app.x = '1';
return h.continue;
};
server.ext('onRequest', onRequest);
const preAuth = (request, h) => {
request.app.x += '2';
return h.continue;
};
server.ext('onPreAuth', preAuth);
const postAuth = (request, h) => {
request.app.x += '3';
return h.continue;
};
server.ext('onPostAuth', postAuth);
const preHandler = (request, h) => {
request.app.x += '4';
return h.continue;
};
server.ext('onPreHandler', preHandler);
const postHandler = (request, h) => {
request.response.source += '5';
return h.continue;
};
server.ext('onPostHandler', postHandler);
const preResponse = (request, h) => {
request.response.source += '6';
return h.continue;
};
server.ext('onPreResponse', preResponse);
server.route({
method: 'GET',
path: '/',
handler: (request) => request.app.x
});
const res = await server.inject('/');
expect(res.result).to.equal('123456');
});
it('combine connection extensions (route first)', async () => {
const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
handler: (request) => request.app.x
});
const onRequest = (request, h) => {
request.app.x = '1';
return h.continue;
};
server.ext('onRequest', onRequest);
const preAuth = (request, h) => {
request.app.x += '2';
return h.continue;
};
server.ext('onPreAuth', preAuth);
const postAuth = (request, h) => {
request.app.x += '3';
return h.continue;
};
server.ext('onPostAuth', postAuth);
const preHandler = (request, h) => {
request.app.x += '4';
return h.continue;
};
server.ext('onPreHandler', preHandler);
const postHandler = (request, h) => {
request.response.source += '5';
return h.continue;
};
server.ext('onPostHandler', postHandler);
const preResponse = (request, h) => {
request.response.source += '6';
return h.continue;
};
server.ext('onPreResponse', preResponse);
const res = await server.inject('/');
expect(res.result).to.equal('123456');
});
it('combine connection extensions (route middle)', async () => {
const server = Hapi.server();
const onRequest = (request, h) => {
request.app.x = '1';
return h.continue;
};
server.ext('onRequest', onRequest);
const preAuth = (request, h) => {
request.app.x += '2';
return h.continue;
};
server.ext('onPreAuth', preAuth);
const postAuth = (request, h) => {
request.app.x += '3';
return h.continue;
};
server.ext('onPostAuth', postAuth);
server.route({
method: 'GET',
path: '/',
handler: (request) => request.app.x
});
const preHandler = (request, h) => {
request.app.x += '4';
return h.continue;
};
server.ext('onPreHandler', preHandler);
const postHandler = (request, h) => {
request.response.source += '5';
return h.continue;
};
server.ext('onPostHandler', postHandler);
const preResponse = (request, h) => {
request.response.source += '6';
return h.continue;
};
server.ext('onPreResponse', preResponse);
const res = await server.inject('/');
expect(res.result).to.equal('123456');
});
it('combine connection extensions (mixed sources)', async () => {
const server = Hapi.server();
const preAuth1 = (request, h) => {
request.app.x = '1';
return h.continue;
};
server.ext('onPreAuth', preAuth1);
server.route({
method: 'GET',
path: '/',
options: {
ext: {
onPreAuth: {
method: (request, h) => {
request.app.x += '2';
return h.continue;
}
}
},
handler: (request) => request.app.x
}
});
const preAuth3 = (request, h) => {
request.app.x += '3';
return h.continue;
};
server.ext('onPreAuth', preAuth3);
server.route({
method: 'GET',
path: '/a',
handler: (request) => request.app.x
});
const res1 = await server.inject('/');
expect(res1.result).to.equal('123');
const res2 = await server.inject('/a');
expect(res2.result).to.equal('13');
});
it('skips inner extensions when not found', async () => {
const server = Hapi.server();
let state = '';
const onRequest = (request, h) => {
state += 1;
return h.continue;
};
server.ext('onRequest', onRequest);
const preAuth = (request) => {
state += 2;
return 'ok';
};
server.ext('onPreAuth', preAuth);
const preResponse = (request, h) => {
state += 3;
return h.continue;
};
server.ext('onPreResponse', preResponse);
const res = await server.inject('/');
expect(res.statusCode).to.equal(404);
expect(state).to.equal('13');
});
});
describe('rules', () => {
it('compiles rules into config', async () => {
const server = Hapi.server();
server.validator(Joi);
const processor = (rules) => {
if (!rules) {
return null;
}
return { validate: { query: { x: rules.x } } };
};
server.rules(processor);
server.route({ path: '/1', method: 'GET', handler: () => null, rules: { x: Joi.number().valid(1) } });
server.route({ path: '/2', method: 'GET', handler: () => null, rules: { x: Joi.number().valid(2) } });
server.route({ path: '/3', method: 'GET', handler: () => null });
expect((await server.inject('/1?x=1')).statusCode).to.equal(204);
expect((await server.inject('/1?x=2')).statusCode).to.equal(400);
expect((await server.inject('/2?x=1')).statusCode).to.equal(400);
expect((await server.inject('/2?x=2')).statusCode).to.equal(204);
expect((await server.inject('/3?x=1')).statusCode).to.equal(204);
expect((await server.inject('/3?x=2')).statusCode).to.equal(204);
});
it('compiles rules into config (route info)', async () => {
const server = Hapi.server();
const processor = (rules, { method, path }) => {
return { app: { method, path, x: rules.x } };
};
server.rules(processor);
server.route({ path: '/1', method: 'GET', handler: (request) => request.route.settings.app, rules: { x: 1 } });
expect((await server.inject('/1')).result).to.equal({ x: 1, path: '/1', method: 'get' });
});
it('compiles rules into config (validate)', () => {
const server = Hapi.server();
server.validator(Joi);
const processor = (rules) => {
return { validate: { query: { x: rules.x } } };
};
server.rules(processor, { validate: { schema: { x: Joi.number().required() } } });
server.route({ path: '/1', method: 'GET', handler: () => null, rules: { x: 1 } });
expect(() => server.route({ path: '/2', method: 'GET', handler: () => null, rules: { x: 'y' } })).to.throw(/must be a number/);
});
it('compiles rules into config (validate + options)', () => {
const server = Hapi.server();
server.validator(Joi);
const processor = (rules) => {
return { validate: { query: { x: rules.x } } };
};
server.rules(processor, { validate: { schema: { x: Joi.number().required() }, options: { allowUnknown: false } } });
server.route({ path: '/1', method: 'GET', handler: () => null, rules: { x: 1 } });
expect(() => server.route({ path: '/2', method: 'GET', handler: () => null, rules: { x: 1, y: 2 } })).to.throw(/is not allowed/);
});
it('cascades rules into configs', async () => {
const handler = (request) => {
return request.route.settings.app.x + ':' + Object.keys(request.route.settings.app).join('').slice(0, -1);
};
const p1 = {
name: 'p1',
register: async (srv) => {
const processor = (rules) => {
return { app: { x: '1+' + rules.x, 1: true } };
};
srv.rules(processor);
await srv.register(p3);
srv.route({ path: '/1', method: 'GET', handler, rules: { x: 1 } });
}
};
const p2 = {
name: 'p2',
register: (srv) => {
const processor = (rules) => {
return { app: { x: '2+' + rules.x, 2: true } };
};
srv.rules(processor);
srv.route({ path: '/2', method: 'GET', handler, rules: { x: 2 } });
}
};
const p3 = {
name: 'p3',
register: async (srv) => {
const processor = (rules) => {
return { app: { x: '3+' + rules.x, 3: true } };
};
srv.rules(processor);
await srv.register(p4);
srv.route({ path: '/3', method: 'GET', handler, rules: { x: 3 } });
}
};
const p4 = {
name: 'p4',
register: async (srv) => {
await srv.register(p5);
srv.route({ path: '/4', method: 'GET', handler, rules: { x: 4 } });
}
};
const p5 = {
name: 'p5',
register: (srv) => {
const processor = (rules) => {
return { app: { x: '5+' + rules.x, 5: true } };
};
srv.rules(processor);
srv.route({ path: '/5', method: 'GET', handler, rules: { x: 5 } });
srv.route({ path: '/6', method: 'GET', handler, rules: { x: 6 }, config: { app: { x: '7' } } });
}
};
const server = Hapi.server();
const processor0 = (rules) => {
return { app: { x: '0+' + rules.x, 0: true } };
};
server.rules(processor0);
await server.register([p1, p2]);
server.route({ path: '/0', method: 'GET', handler, rules: { x: 0 } });
expect((await server.inject('/0')).result).to.equal('0+0:0');
expect((await server.inject('/1')).result).to.equal('1+1:01');
expect((await server.inject('/2')).result).to.equal('2+2:02');
expect((await server.inject('/3')).result).to.equal('3+3:013');
expect((await server.inject('/4')).result).to.equal('3+4:013');
expect((await server.inject('/5')).result).to.equal('5+5:0135');
expect((await server.inject('/6')).result).to.equal('7:0135');
});
});
describe('drain()', () => {
it('drains the request payload on 404', async () => {
const server = Hapi.server();
const res = await server.inject({ method: 'POST', url: '/nope', payload: 'something' });
expect(res.statusCode).to.equal(404);
expect(res.raw.req._readableState.ended).to.be.true();
});
});
});
================================================
FILE: test/security.js
================================================
'use strict';
const Code = require('@hapi/code');
const Hapi = require('..');
const Lab = require('@hapi/lab');
const internals = {};
const { describe, it } = exports.lab = Lab.script();
const expect = Code.expect;
describe('security', () => {
it('handles missing routes', async () => {
const server = Hapi.server({ port: 8080, routes: { security: { xframe: true } } });
const res = await server.inject('/');
expect(res.statusCode).to.equal(404);
expect(res.headers['x-frame-options']).to.exist();
});
it('blocks response splitting through the request.create method', async () => {
const server = Hapi.server();
const handler = (request, h) => h.response('Moved').created('/item/' + request.payload.name);
server.route({ method: 'POST', path: '/item', handler });
const res = await server.inject({
method: 'POST', url: '/item',
payload: '{"name": "foobar\r\nContent-Length: \r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 19\r\n\r\nShazam"}',
headers: { 'Content-Type': 'application/json' }
});
expect(res.statusCode).to.equal(400);
});
it('prevents xss with invalid content types', async () => {
const server = Hapi.server();
server.state('encoded', { encoding: 'iron' });
server.route({
method: 'POST', path: '/',
handler: () => 'Success'
});
const res = await server.inject({
method: 'POST',
url: '/',
payload: '{"something":"something"}',
headers: { 'content-type': ';' }
});
expect(res.result.message).to.not.contain('script');
});
it('prevents xss with invalid cookie values in the request', async () => {
const server = Hapi.server();
server.state('encoded', { encoding: 'iron' });
server.route({
method: 'POST', path: '/',
handler: () => 'Success'
});
const res = await server.inject({
method: 'POST',
url: '/',
payload: '{"something":"something"}',
headers: { cookie: 'encoded="";' }
});
expect(res.result.message).to.not.contain('=value;' }
});
expect(res.result.message).to.not.contain('