Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rethink/Reorganise HTTP error structure #4101

Open
shafreenAnfar opened this issue Feb 17, 2023 · 5 comments · Fixed by ballerina-platform/module-ballerina-http#1487
Open
Assignees
Labels
module/http Points/4 Team/PCM Protocol connector packages related issues Type/Improvement
Milestone

Comments

@shafreenAnfar
Copy link
Contributor

Model of the HTTP error handling hierarchy is something like the below.

Tmp

Usual purpose of these errors is to represent expected but undesirable results of functions. Usual actions of these error messages are,

  • Log the message to build a story to debug issues
  • Propagate to higher level by adding context
  • Take actions like retry if it is a transient failure
  • Ignore

In the case of the HTTP module, there is this ADDITIONAL requirement to send a HTTP response when these errors propagate to the listener. These responses attributes such as,

  • Headers
  • Body
  • Status codes

To cater to this additional requirement we can model a parallel error structure as follows.

Additional error structure

Then we can connect to two models as follows,

Model error structure

Here the default error is the error used to represent all the errors. It is similar to switch-case-default relationship. See this for more details.

https://swagger.io/docs/specification/describing-responses/

Following is the sample code.

import ballerina/time;
import ballerina/io;

// Error model to represent wire requirements
public type Detail record {
    int statusCode?;
    map<string[]> headers?;
    anydata body;
};
type HttpWireError distinct error<Detail>;
type HttpNotFoundError distinct HttpWireError;

// Error model to represent usual application\listener requirements
type RootError distinct error;

type ErrorDetails record {|
    time:Utc timeStamp;
    string message;
    string details;
|};
type MyErrorDetails record {
    *Detail;
    ErrorDetails body;
};
type DispatchingError HttpNotFoundError & RootError & error<MyErrorDetails>;

public function main() {
    ErrorDetails errDetails = {timeStamp: time:utcNow(), message: "File not found", details: "foo.txt"};
    DispatchingError err = error DispatchingError("File not found", body = errDetails);

    if err is DispatchingError {
        io:println("DispatchingError");
    }
    if err is HttpNotFoundError {
        io:println("HttpNotFoundError");
    }
    if err is RootError {
        io:println("RootError");
    }
}
@shafreenAnfar shafreenAnfar added Type/Improvement module/http Team/PCM Protocol connector packages related issues labels Feb 17, 2023
@shafreenAnfar
Copy link
Contributor Author

Also this means if someone returns an error that has no association to the green error structure, it should be defaulted to Default error in the green error structure. Therefore, when generating OAS for error, the status should be default not 500.

@shafreenAnfar
Copy link
Contributor Author

shafreenAnfar commented Jun 14, 2023

We decided to move these HTTP status code errors to httpscerr sub module.

In addition, following is an example of DefaultHttpStatusCodeError usage in combination of HTTP status code errors.

public type User record {|
    string id;
    string name;
|};

public type MyErrorPayload record {|
    time:Utc timeStamp;
    string message;
    string details;
|};
public type MyDetaultErrorDetail record {
    *HttpDefaultErrorDetail;
    MyErrorPayload body;
};
type MyHttpDefaultStatusCodeError HttpDefaultStatusCodeError & error<MyDetaultErrorDetail>;

service /dept on new http:Listener(9090) {
    resource function get user/[string id]() returns User|MyHttpDefaultStatusCodeError {
        do {
            return check getUserName(id);
        } on fail error err {
            MyDetaultErrorDetail errDetails = {
                timeStamp: time:utcNow(), 
                message: "User not found", 
                details: string `/dept/user/${id}`
            };
            return error MyHttpDefaultStatusCodeError("Error occurred while processing the request", err, body = errDetails);
        }
    }
}

function getUserName(string id) returns User|UserNotFoundError {
    if (id == "1") {
        return {id: "1", name: "John"};
    } else {
        return error UserNotFoundError("User not found");
    }
}

type UserNotFoundError error & HttpNotFoundError;

@shafreenAnfar
Copy link
Contributor Author

shafreenAnfar commented Jun 26, 2023

Following is how OpenAPI looks like for the above code.

openapi: 3.0.1
info:
  title: Dept
  version: 0.0.0
servers:
- url: "{server}:{port}/dept"
  variables:
    server:
      default: http://localhost
    port:
      default: "9090"
paths:
  /user/{id}:
    get:
      operationId: getUserId
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        "200":
          description: Ok
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        "default":
          description: Default error for user API
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MyDetaultErrorDetail'
components:
  schemas:
    User:
      required:
      - id
      - name
      type: object
      properties:
        id:
          type: string
        name:
          type: string
    MyDetaultErrorDetail:
      required:
      - message
      - details
      type: object
      properties:
        code:
          type: string
        message:
          type: string

@shafreenAnfar
Copy link
Contributor Author

shafreenAnfar commented Jun 30, 2023

In cases where errors are originated from libraries, the code would like the below.

service /dept on new http:Listener(9090) {
    resource function get user/[string id]() returns User|MyHttpDefaultStatusCodeError {
        do {
            string validId = check constraint:validate(id);
            return check getUserName(validId);
        } on fail error err {
            MyErrorPayload errDetails = {
                timeStamp: time:utcNow(), 
                message: "User not found", 
                details: string `/dept/user/${id}`
            };
            if err is constraint:Error {
                return error MyHttpDefaultStatusCodeError("Error occurred while processing the request", err, body = errDetails, statusCode = 400);
            }
            return error MyHttpDefaultStatusCodeError("Error occurred while processing the request", err, body = errDetails);
        }
    }
}

@shafreenAnfar
Copy link
Contributor Author

shafreenAnfar commented Jun 30, 2023

Current code would look like this.

service /dept on new http:Listener(9090) {
    resource function get user/[string id]() returns User|MyUserNotFound|MyBadRequest|MyInternelServerError {
        do {
            string validId = check constraint:validate(id);
            return check getUserName(validId);
        } on fail error err {
            MyErrorPayload errDetails = {
                timeStamp: time:utcNow(), 
                message: "User not found", 
                details: string `/dept/user/${id}`
            };
            if err is constraint:Error {
                return <MyBadRequest>{body: errDetails};
            } else if err is UserNotFoundError {
                return <MyUserNotFound>{body: errDetails};
            } else {
                return <MyInternelServerError>{body: errDetails};
            }
        }
    }
}

type UserNotFoundError distinct error;

function getUserName(string id) returns User|error {
    if (id == "1") {
        return {id: "1", name: "John"};
    } else {
        return error ("User not found");
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
module/http Points/4 Team/PCM Protocol connector packages related issues Type/Improvement
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

2 participants