Future Studio started with just a blog. We’ve used Ghost to write, publish and manage all the tutorials. While extending the blog to a comprehensive learning platform, we chose hapi as the foundational framework. While moving the functionality to show tutorials from Ghost to our own hapi server, we’ve recognized some „strange“ behavior in hapi with request URLs that end on a slash.
In the following, you’ll learn how to properly handle request URLs having a trailing slash.
hapi Series Overview
- What You’ll Build
- Prepare Your Project: Stack & Structure
- Environment Variables and Storing Secrets
- Set Up MongoDB and Connect With Mongoose
- Sending Emails in Node.js
- Load the User’s Profile Picture From Gravatar Using Virtuals in Mongoose
- Implement a User Profile Editing Screen
- Generate a Username in Mongoose Middleware
- Displaying Seasons and Episodes for TV Shows with Mongoose Relationship Population
- Implementing Pagination for Movies
- Implement a Watchlist
- Create a Full Text Search with MongoDB
- Create a REST API with JSON Endpoints
- Update Mongoose Models for JSON Responses
- API Pagination for TV Shows
- Customize API Endpoints with Query Parameters
- Always Throw and Handle API Validation Errors
- Advanced API Validation With Custom Errors
- Create an API Documentation with Swagger
- Customize Your Swagger API Documentation URL
- Describe Endpoint Details in Your Swagger API Documentation
- 10 Tips on API Testing With Postman
- JWT Authentication in Swagger API Documentation
- API Versioning with Request Headers
- API Login With Username and Password to Generate a JWT
- JWT Authentication and Private API Endpoints
- Refresh Tokens With JWT Authentication
- Create a JWT Utility
- JWT Refresh Token for Multiple Devices
- Check Refresh Token in Authentication Strategy
- Rate Limit Your Refresh Token API Endpoint
- How to Revoke a JWT
- Invalidate JWTs With Blacklists
- JWT Logout (Part 1/2)
- JWT “Immediate” Logout (Part 2/2)
- A Better Place to Invalidate Tokens
- How to Switch the JWT Signing Algorithm
- Roll Your Own Refresh Token Authentication Scheme
- JWT Claims 101
- Use JWT With Asymmetric Signatures (RS256 & Co.)
- Encrypt the JWT Payload (The Simple Way)
- Increase JWT Security Beyond the Signature
- Unsigned JSON Web Tokens (Unsecured JWS)
- JWK and JWKS Overview
- Provide a JWKS API Endpoint
- Create a JWK from a Shared Secret
- JWT Verification via JWKS API Endpoint
- What is JOSE in JWT
- Encrypt a JWT (the JWE Way)
- Authenticate Encrypted JWTs (JWE)
- Encrypted and Signed JWT (Nested JWT)
- Bringing Back JWT Decoding and Authentication
- Bringing Back JWT Claims in the JWT Payload
- Basic Authentication With Username and Password
- Authentication and Remember Me Using Cookies
- How to Set a Default Authentication Strategy
- Define Multiple Authentication Strategies for a Route
- Restrict User Access With Scopes
- Show „Insufficient Scope“ View for Routes With Restricted Access
- Access Restriction With Dynamic and Advanced Scopes
- hapi - How to Fix „unknown authentication strategy“
- Authenticate with GitHub And Remember the Login
- Authenticate with GitLab And Remember the User
- How to Combine Bell With Another Authentication Strategy
- Custom OAuth Bell Strategy to Connect With any Server
- Redirect to Previous Page After Login
- How to Implement a Complete Sign Up Flow With Email and Password
- How to Implement a Complete Login Flow
- Implement a Password-Reset Flow
- Views in hapi 9 (and above)
- How to Render and Reply Views
- How to Reply and Render Pug Views (Using Pug 2.0)
- How to Create a Dynamic Handlebars Layout Template
- Create and Use Handlebars Partial Views
- Create and Use Custom Handlebars Helpers
- Specify a Different Handlebars Layout for a Specific View
- How to Create Jade-Like Layout Blocks in Handlebars
- Use Vue.js Mustache Tags in Handlebars Templates
- How to Use Multiple Handlebars Layouts
- How to Access and Handle Request Payload
- Access Request Headers
- How to Manage Cookies and HTTP States Across Requests
- Detect and Get the Client IP Address
- How to Upload Files
- Quick Access to Logged In User in Route Handlers
- How to Fix “handler method did not return a value, a promise, or throw an error”
- How to Fix “X must return an error, a takeover response, or a continue signal”
- Query Parameter Validation With Joi
- Path Parameter Validation With Joi
- Request Payload Validation With Joi
- Validate Query and Path Parameters, Payload and Headers All at Once on Your Routes
- Validate Request Headers With Joi
- Reply Custom View for Failed Validations
- Handle Failed Validations and Show Errors Details at Inputs
- How to Fix AssertionError, Cannot validate HEAD or GET requests
Problem
In our case, we just noticed this issue due to the fact that Ghost generates all URLs with a trailing slash. While transitioning the functionality to show tutorials from Ghost to our own platform, we —of course— wanted to make sure all URLs out there are still accessible and route to the correct content. And to do that, we need to handle URLs that end on a trailing slash accordingly.
Let’s showcase the problem with a simple example: create a hapi server and register the following two routes.
server.route([
{
method: 'GET',
path: '/tutorials',
handler: function (request, reply) {
reply('Called the /tutorials route, without trailing slash')
}
},
{
method: 'GET',
path: '/tutorials/{slug}',
handler: function (request, reply) {
reply('Called route with a trailing slash and path param: ' + request.params.name)
}
}
])
Now call the following URL:
http://localhost:3000/tutorials/
You’ll notice a 404 error that indicates the given route is not available on your server.
{
"statusCode":404,
"error":"Not Found"
}
Well, that’s a bummer. We want to keep the two routes separated, because they are logically different and each deliver completely different content.
The Bad Solution
The „bad solution“ is to just use a single route with an optional path parameter instead of two separate routes. Within the route handler, you differenciate between the two situations: whether a slug is available or not.
server.route(
{
method: 'GET',
path: '/tutorials/{slug?}',
handler: function (request, reply) {
if (!request.params.slug) {
return reply('Called the /tutorials or /tutorials/ route')
}
reply('Called route with slash at the end: ' + request.params.name)
}
}
)
You can immediately spot the problem: hiding functionalities for different routes behind a single route handler will definitely lead to issues. Especially if you’re working in a team there will be complaints by your peers to improve this situation.
We agree, there needs to be a better solution. And actually there is! Read the next section to learn about a clean and sexy way to configure your routes individually and have separated route handlers.
Good Solution
Actually, you can grab the issue directly by its roots. Hapi has built-in functionalities to strip trailing slashes on route paths. That allows you to access URLs with a trailing slash as the pendants without the slash.
The following code block outlines the hapi server configuration to ignore trailing slashes on request URLs:
var Hapi = require('hapi')
// create new server instance
var server = new Hapi.Server()
// add server’s connection information
server.connection({
host: 'localhost',
port: 3000,
router: {
stripTrailingSlash: true
}
})
Using the configuration above, you can split your routes into separate definitions with their individual handler functions. This way, you don’t hide complex processing behind an optional path parameter on a single route.
Add the server configuration from above and two routes from the beginning of this tutorial to your server and start the app. Now visit http://localhost:3000/tutorials/
and see that the trailing slash is ignored by hapi and the correct route handler gets called.
Outlook
The problem of trailing slashes on route paths can be kind of annoying and different platforms generate both URL styles: with and without trailing slashes.
Hapi has built-in support to handle this situation correctly, you just need to activate it in the server’s router configuration.
Hopefully you didn’t go gray from being angry about hapi’s default behavior on trailing slashes!
If you’ve feedback or questions, please don‘t hesitate to reach us on Twitter @futurestud_io or use the comments below. We appreciate any message, success story, or question. Just shout out :)
Make it rock & enjoy coding!