How to prevent unique constraint on username from registerPostHandler throwing an internal server error

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.

What and how can we circumvent this issue?

1 Like

Apologies. I just figured that we can check if the username already exists before saving the record.

If the user exists, we can redirect back to the registration page with an error message in the view.

@michaeltansg Glad you figured it out! :]

From your solution, here is my code, it’s working but im not sure this is right way or not … hope i can see your code to learn…

image
image

image

i’ll welcome if you can optimize my code. @michaeltansg

Hi @choioi.

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.

1 Like

thanks you, i’ll try with ErrorMiddleware, i want generic a json with success for all reponse json but dont know how to start :slight_smile:

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:

    services.register { worker in
        return try ErrorMiddleware.customErrorHandler(environment: worker.environment, log: worker.make())
    }

HTH! :slight_smile:

1 Like