Coldbox and VueJS untangled

Problem Details for HTTP APIs: Modifying your Coldbox REST handler response

Coldbox had a base handler and response for RESTful services for many years. Initially this was added in application templates but in version 6 this functionality was added to the core. The base handler wraps around your own actions and provides a lot of automatic errorhandling, addition of some development headers and some global headers. By using the default event.getResponse() response (available as prc.response in previous versions) it provides a default response format which looks like this:

{
    "data": {
        "name": "someData"
        "id": "98765",
        "name": "provMan"
    },
    "error": false,
    "pagination": {
         "totalPages": 1,
         "maxRows": 0,
         "offset": 0,
         "page": 1,
         "totalRecords": 0
    },
    "messages": []
}

That’s a lot of different keys for a default response format. The data field makes sense most of the time, and the pagination key (which is new in cb6) can be handy but we don’t use it (yet). The error and messages keys are less useful to us. Let me explain why first, and then I will explain how to modify your responses in coldbox 6.

When our API is returning an error the client should report this in some way, or even correct the error, based on the information which is returned by the API. This often means: return the HTTP status code and show the error message. But sometimes it means more: on validation errors for example we want to have the 400 status code, but also a list of failing form fields and some messages why they are failing, or even the failing value. This can be handled in an array of structs, but the messages key only accepts strings. Another case is when we have failing authentication. This can happen when people are not sending in the correct authentication headers, making errors when typing passwords or even trying to submit failing two factor authentication codes. All three error conditions should raise the same http error code 401 but they all need different handling in our system. So more detail is needed. We could put this detail in the data key, but we think you shouldn’t put metadata in such a key. So that’s why we wanted a different format for errors.

Many companies handle errors in their own proprietary way. For some examples (Google, Spotify, Facebook) this blogpost is an interesting read. But actually there is a standard defined for this, and is this described in RFC 7807. Most RFC’s are long, boring and hard to read, but this one is short. But let me summarize it for you.

  • all errors should return a different mime type compared to your regular responses, which means application/problem+json instead of application/json. (The RFC describes a simular approach for XML).
  • all error responses can have the the following members
    • type: this is an URI describing the problem type
    • title: a description which can be used for display.
    • status: the original HTTP status code
    • (optional) detail: a human readable explanation specific for the occurrence of this problem
    • (optional) instance: a URI reference that identifies the specific occurrence of this problem
  • Extension members: each specific problem can have it’s own additional fields specifying more information about the problem.

An example for an extension member would be a validationErrors key which specifies all individual errors. The RFC has some more useful examples.

The advantage off such an approach: by specifying a special mime type it is almost trivial for client-side libraries like Axios to distinguish between errors and ‘real’ data. Even if your HTTP response code is the same, you can still specify a different error as specified in the type field. This field is a unique URI, where you can describe your errors and how to handle them. This is not for display to endusers, but to offer your front-end developers some help. The extension members can offer a lot of additional info, which can help to resolve your problem automatically, of in case of validation show useful information for each failed form field. And finally: it is a standard, so you know what to expect (by reading this RFC).

I am absolutely sure not all people want to replace the Ortus standard by this RFC, but hey, as usually coldbox offers enough hooks to customize our response.

The proces is quite simple:

  • create your custom response object or extend coldbox.system.web.context.Response (as we did).
  • replace the default response handler by your custom version as described in the manual, which means
    • create an interceptor for preProcess()
    • load your own response object in this preProcess interceptor like this
      prc.response = wirebox.getInstance( "MyResponseObject" );
    • don’t forget to register your interceptor in config/Coldbox.cfc
    • and please read the fine manual on this. It has very clear examples.
  • create the necessary methods in your just created custom response object.
  • and optionally: extend the coldbox.system.RestHandler only if you want to override the around handler or some of the onSomeExceptions methods.

I’ll show you some code how we modified the response. Our response object looks like this and the most important part is the getDataPacket() method:

component extends="coldbox.system.web.context.Response" accessors="true" {
	
  // Properties
  property name="type" type="string" default ="about:blank";
  property name="title" type="string" default="";
  property name="detail" type="string" default="";
  property name="instance" type="string" default="";
  property name="extensions" type ="struct" ;

  property name="event" inject="coldbox:requestContext";

  // HTTP STATUS TEXTS - necessary to specify some status text
  this.STATUS_TEXTS = {
    "200" : "OK",
    "201" : "Created",
    "400" : "Bad Request",
    "401" : "Unauthorized",
    "403" : "Forbidden",
    "404" : "Not Found",
    "500" : "Internal Server Error"
    // you might need more codes...
  };
  /**
   * Constructor
   */
  CustomRestResponse function init(){
    super.init();
    variables.type="about:blank";
    variables.title="";
    variables.detail="";
    variables.instance="";
    // extension struct added for flexible extra keys
    variables.extensions = {};
    return this;
  }
  // you can add extension members here (aka extra keys)
  function addExtension( required string key, required any extension ){
    variables.extensions[arguments.key] = arguments.extension;
    return this;
  }

  function getDataPacket() {
    if ( !variables.error){
      return packet = super.getDataPacket();
    } else {
      variables.statusText = this.STATUS_TEXTS[variables.StatusCode];
      variables.instance = event.getCurrentRoutedURL();
      variables.contentType = "application/problem+json";
      var packet = {
        "type"= variables.type,
        "title"= len(variables.title) ? variables.title : this.STATUS_TEXTS[variables.StatusCode],
        "detail"= variables.detail,
        "instance"= variables.instance,
        "status"= variables.statuscode,
      }
      // add extension keys to the datapacket
      variables.extensions.each((key,value)=>{
        packet[key]=value;
      })
    };
    return packet;
  }
  // convenience method to add specific errors
  function setErrorDetails(
    required numeric statusCode,
    required string type =  "about:blank",
    string title = "",
    string detail = ""
  ){
    variables.statusCode = statusCode
    variables.error = true;
    variables.statusText = this.STATUS_TEXTS[arguments.StatusCode];
    variables.type=arguments.type;
    variables.title = arguments.title;
    variables.detail = arguments.detail;
    return this;
  }
}

The getDataPacket() method in the original RestHandler will be used to create your response. By overriding this method in your own Response object you can modify the response for error handling. We do so by checking the error flag and if there is an error we are generating a different datapacket

Finally we also want to override the onValidationException in the RestHandler, because we want to add the validation errors in our error response. So we copied the method from the RestHandler to our own (extended) Resthandler and modified a small part like this:

// Setup Response
  arguments.event
    .getResponse()
    .setError( false )
    .setErrorDetails(
      statusCode = event.STATUS.BAD_REQUEST,
      type = "api-errors/validation-error",
        title = $r("VALIDATION_ERROR@validation"),
        instance = event.getCurrentRoutedURL()
    )
    .addExtension(
      "validationErrors", 
      isJSON( arguments.exception.extendedInfo ) ? deserializeJSON( arguments.exception.extendedInfo ) : ""
    )

So this way, we created our own a-la-carte API response. 100% compatible for all regular responses and a customized error response for everything else. Some examples screenshots:

A validation error
An authentication error for invalid username/password

Don’t be shy, and have a look at the coldbox source code to see what’s going on under the hood. And as always: coldbox can make your life a lot easier, and if you don’t like it, it is flexible enough to create your own solution!

2 Comments

  1. Sulman A

    Hi,

    Added a security layer by following this ColdBox restful documentation:
    https://coldbox.ortusbooks.com/digging-deeper/recipes/building-rest-apis#custom

    Added interceptors and other database checks everything seems to be working fine.

    Below code line is by which I’m setting the unauthorize response.

    event.getResponse().setError( true ).setStatusText( ‘Please check your credentials’ ).setStatusCode( 401 ).addMessage( “Remote server is not allowed” ).setData( {} );

    The issue is when I set statusCode 401 got error but when set 200 it working fine.

    When set setStatusCode( 200 ) it’s working.

    But when set setStatusCode( 401 ) got this error:
    Response to preflight request doesn’t pass access control check: It does not have HTTP ok status.

    Thanks!

    • Wil

      I think this issue is not related to the topic of my Post. This error seems to surface when your CORS headers are not setup correctly.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2024 ShiftInsert.nl

Theme by Anders NorenUp ↑