Most registration pages would let you know that the username is already being used. If we create a new user with a username that has already been used, the database unique constraint would throw an internal server error with a content type of application/json and a payload of: {"error":true,"reason":"duplicate key value violates unique constraint \"uq:User.username\""}.
This being a web app, the application/json response is most unusual user experience.
The issue I am addressing is for the WebController’s registerPostHandler where users expect to see a web page (or some error message) instead of an application/json response in a web browser when the username already exists.
In order to prevent the database unique constraint from breaking the web app, here is what I did:
func registerPostHandler(_ req: Request, data: RegisterData) throws -> Future<Response> {
do {
try data.validate()
} catch (let error) {
let error = error as? ValidationError
return req.future(req.redirect(to: redirectURL(error?.reason)))
}
return User
.query(on: req)
.filter(\.username == data.username)
.first()
.flatMap(to: Response.self) { foundUser in
guard let _ = foundUser else {
let password = try BCrypt.hash(data.password)
let user = User(name: data.name, username: data.username, password: password)
return user.save(on: req).map(to: Response.self) { user in
try req.authenticateSession(user)
return req.redirect(to: "/")
}
}
return req.future(req.redirect(to: self.redirectURL("The email address has already been registered.")))
}
}
private func redirectURL(_ errorMessage: String?) -> String {
let redirect: String
if let message = errorMessage?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
redirect = "/register?message=\(message)"
} else {
redirect = "/register?message=Unknown+error"
}
return redirect
}
The existing behavior of the API for func createHandler(_ req: Request, user: User) throws -> Future<User.Public> is a proper application/json response.
The extra check to verify if the user already exists, in my opinion, is unnecessary. If your intention is to create a consistent JSON payload for any errors, then I would propose extending ErrorMiddleware with your custom error handler.
Firstly, make sure you have registered the ErrorMiddleWare:
/// Register middleware
var middlewares = MiddlewareConfig() // Create _empty_ middleware config
.
.
middlewares.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response
Then take a look at public static func ``default``(environment: Environment, log: Logger) -> ErrorMiddleware for some idea on the default implementation. This is in the ErrorMiddleware.swift file in your dependencies.
Next create an extension with your custom error handler (the code fragment below assumes your custom error handler is named customErrorHandler):
extension ErrorMiddleware {
public static func customErrorHandler(environment: Environment, log: Logger) -> ErrorMiddleware {
return .init { req, error in
.
.
// Your implementation goes here
.
.
}
}
}
Finally make sure that you register the service with a closure that allows you to inject the environment and logger like so: