Sunday, May 21, 2023

Generic Errors - Cloud Native Microservices with Kotlin Spring-Boot

 Generic errors

Sometimes APIs can return just generic errors, in many cases, regardless of the verb used; the more common ones are: 

STATUS MEANING = 400 BAD REQUEST 

We could not answer the request because it is incorrect 

401 UNAUTHORIZED 

We don't have the credentials for that operation 

403 FORBIDDEN 

We may have the credentials but we are not allowed to do that operation  

422 UNPROCESSABLE ENTITY 

We could not process the request; it may be correct, but it is not valid for this operation 

500 INTERNAL SERVER ERROR We have not been able to process the request A 500 error is really something critical in our application, sometimes it means that it could not even recover from it, such as losing connection to a database or another system. We should never return any 5xx range error with a functional meaning, use 4xx ranges instead.

Handling HTTP verbs 

Spring allows us to define the HTTP verbs that our controller method will handle. We do this using the parameter method in our @RequestMapping. For example, in our controller: @RequestMapping(value = "/customer/{id}", method = arrayOf(RequestMethod.GET))

fun getCustomer(@PathVariable id: Int) = customers[id] 

This parameter is actually an array, it can be changed, if required, to accept more than one method. For example, as: 

@RequestMapping(value = "/customer/{id}", method = arrayOf(RequestMethod.GET, RequestMethod.POST))

fun getCustomer(@PathVariable id: Int) = customers[id] 

With this change, we set that we will accept either HTTP GET or HTTP POST in this method, however, we recommend you keep one method per function in your controller. If we follow our recommendation, GET will get a resource, and a POST will create a resource, then our method will try to do two different things, and for that, we should have two different methods. We should always remember the single responsibility principle when designing our method. A method should have only one reason to change. We already defined how we can handle our HTTP GET in our previous examples. let's review how we can handle other HTTP verbs.

Handling HTTP POST 

We will use HTTP POST when we try to create a resource, so if we try to add new customers, we will post a new resource to our customer URL /customer/. We can do this simply with this code: 

package com.microservices

import org.springframework.beans.factory.annotation.Autowired

import org.springframework.web.bind.annotation.*

import java.util.concurrent.ConcurrentHashMap

 @RestController

class CustomerController {

  @Autowired

  lateinit var customers : ConcurrentHashMap<Int, Customer>

 @RequestMapping(value = "/customers", method = arrayOf(RequestMethod.GET))

  fun getCustomers() = customers.map(Map.Entry<Int, Customer>::value).toList()

 @RequestMapping(value = "/customer/{id}", method = arrayOf(RequestMethod.GET))

 fun getCustomer(@PathVariable id : Int) = customers[id]

 @RequestMapping(value = "/customer/", method = arrayOf(RequestMethod.POST))

  fun createCustomer(@RequestBody customer: Customer) {

    customers[customer.id] = customer

  }

}

 Here, we have used the annotation @RequestBody to specify that we are sending a object. Since this is within a @RESTController, the expected object should be in JSON format. For this example, we can use: 

 {

 "id": 4,

 "name": "New Customer"

}

 We can simply test this request using cURL: curl -X POST \

 http://localhost:8080/customer/ \

 -H 'content-type: application/json' \

 -d '{

 "id": 4,

 "name": "New Customer"

}'

One important thing to consider in that command is that we have set the content-type header to be application/json and this is because we need to in order for Spring to understand that it is a JSON body that we are sending. After executing the command, we can now do a request to a list of customers at the URL http://localhost:8080/customers, to get the following output: [{"id":1,"name":"Kotlin"},{"id":2,"name":"Spring"},{"id":3,"name":"Microservice"},{"id":4,"name":"New Customer"}] One thing that we should remember in this example is that the id of the customer is sent in the object and is not present in the URL, and this follows our recommendations of URLs and verbs at the beginning of this blog.

Handling HTTP DELETE

When we use the HTTP DELETE, we are asking our service to delete a given resource, and we will do it in the form of /customer/id. This specifies which resource needs to be deleted among all of them: 

@RequestMapping(value = "/customer/{id}", method = arrayOf(RequestMethod.DELETE))

fun deleteCustomer(@PathVariable id: Int) = customers.remove(id) 

For this operation, we have just set the corresponding HTTP verb in the method, and as a path variable, the id of the resource to be deleted. Then, we simply remove it from our customer map. We don't need to have any customer as a body parameter since with the id, we could just remove our resource. We could test this operation sending a simple request using cURL: 

curl -X DELETE http://localhost:8080/customer/4

After executing the command, we can now do a request to a list of customers at the URL http://localhost:8080/customers, to get the output: [{"id":1,"name":"Kotlin"},{"id":2,"name":"Spring"},{"id":3,"name":"Microservice"}]

Handling HTTP PUT When we use the HTTP PUT , we are asking our service to update a given resource, and we will do it in the form of /customer/id.This specifies which resource needs to be updated among all of them. But we need to send a customer as JSON body as well so we get exactly what we need to update. In RESTful APIs, the concept is that resources are held in the client and updated back to the server when required. Then, this object represents a state of the object which was queried. But before implementation, we may need to consider one thing. What happens if the resource that we request to update changes the id, is it a valid scenario? It depends on how we have defined our API to work, and it could be actually something that we like to do, so let's implement it in that particular scenario: 

@RequestMapping(value = "/customer/{id}", method = arrayOf(RequestMethod.PUT))

fun updateCustomer(@PathVariable id: Int, @RequestBody customer: Customer) {

  customers.remove(id)

customers[customer.id] = customer

For the implementation of this method, we have just chosen to do what our delete and create does. If we execute this cURL request: 

curl -X PUT \

 http://localhost:8080/customer/2 \

 -H 'cache-control: no-cache' \

 -H 'content-type: application/json' \

 -d '{

 "id": 4,

 "name": "Update Customer"

}' 

After executing the command, we can now do a request to a list of customers at the URL http://localhost:8080/customers, to get an output: [{"id":1,"name":"Kotlin"},{"id":2,"name":"Spring"},{"id":4,"name":"Update Customer"}]

Using verb and mapping annotations 

We have used @RequestMapping in our example, giving the method as a parameter to the annotation, and that is okay but maybe it is too much code for such a simple thing; Spring provides helpers to reduce this declaration: 

@GetMapping(value = "/customer/{id}")

fun getCustomer(@PathVariable id : Int) = customers[id]

 For a GET request, we could use @GetMapping, and we don't need to specify the method; there are equivalent annotations for POST, PUT, and DELETE, @PostMapping, @PutMapping, and @DeleteMapping respectively. Now with this final change, we complete our verbs code, but we can see some level of duplication in our PUT verb which doesn't look right, so we probably need to explore concepts about how we can implement this better.

Empty responses 

In all our methods, we are set to return a JSON response, and this is because we use a RESTController. However, it is interesting to understand that if we are going to return an object, and the returning object is null, the body will be empty, so there will be no content in our response, just status. That is what is happening when we call our get service with a non-existent ID; an empty response is returned. In the method that we declared before that returns Unit, an empty JSON object will be returned instead. If we invoke them, we will get just {}; that is what we get when we call our delete, or update methods with a non-existent ID, an empty object is returned. But we could change the code to return no content even for the Unit methods. If we need to, just modifying them like this: 

@PostMapping(value = "/customer/")

fun createCustomer(@RequestBody customer: Customer): ResponseEntity<Unit?> {

  customerService.createCustomer(customer) 

  return ResponseEntity(null, HttpStatus.CREATED)

But, what should we return? An empty object or an empty response? It is up to us to decide what is better for our API, but we prefer to have empty responses than empty objects since it will be less confusing to the user of the API to get simply no content than an empty object. In Rest API methods, it is a good practice to always return a value, in operations like those described before we could return a simple JSON that says that everything was ok, something like { "result" : "ok" }. This will prevent consumers misunderstanding the response, however, the consumer should always trust the HttpStatus regardless of the response body.


No comments: