1

I've stepped into something I don't understand.

Here is an example of Gin API route handler, written in Golang.


// src/file1.go

func CreateDataModelAction(c *gin.Context) {
    var input inputs.CreateDataModelInput

    if err := c.ShouldBindJSON(&input); err != nil {
        // Here I use a function that generates and returns API responses for validation errors.
        // This function is located in separate file and it's going to be listed below this code        block.
        api.RespondWithValidationError(c, outputs.GetValidationErrors(err))
    return
    }

    c.JSON(http.StatusCreated, nil)
}

// src/file2.go

func RespondWithValidationError(c *gin.Context, validationErrors outputs.DataValidationErrorAPIResponse) {
    c.AbortWithStatusJSON(http.StatusUnprocessableEntity, validationErrors)
}

In case of validation errors happening after ShouldBindJSON I expect this code to enter if block and to return 422 status code with validation errors. However, this code always returns status code 200 despite the fact thic function can only return 422 or 201.

But if I remove api.RespondWithValidationError function and use AbortWithStatusJSON function directly in route handler, I receive the status code 422 as it is expected.


// This example works as expected

func CreateDataModelAction(c *gin.Context) {
    var input inputs.CreateDataModelInput

    if err := c.ShouldBindJSON(&input); err != nil {
        c.AbortWithStatusJSON(http.StatusUnprocessableEntity)
    return
    }

    c.JSON(http.StatusCreated, nil)
}

I was trying to read Gin context source code in order to understand why this is happening, and I have not succeeded so far.

Please explain to me how exactly Gin context works in this case and why I'm not receiving 422 when I'm returning 422 status code from separate function.

Andrey Korchak
  • 2,280
  • 1
  • 18
  • 15
  • Can you provide a minimal reproducer? I don't believe the current code behaves as the description. – Zeke Lu Jun 20 '23 at 07:56

1 Answers1

1

I put together a small example based on the code of your question. I guessed the details you omitted but the relevant code should be good. I put all of the code in a single file just for the sake of the demo and to be more concise.

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

// input DTO provided by HTTP Request
type createDataModeInput struct {
    Name string `json:"name" binding:"required"`
}

// output DTO to send in error scenario
type DataValidationErrorApiResponse struct {
    Code    string `json:"code"`
    Message string `json:"message"`
}

// function to build the DataValidationErrorApiResponse instance
func GetValidationErrors(err error) DataValidationErrorApiResponse {
    return DataValidationErrorApiResponse{
        Code:    "VALIDATION_ERR",
        Message: err.Error(),
    }
}

func RespondWithValidationError(c *gin.Context, validationErrors DataValidationErrorApiResponse) {
    c.AbortWithStatusJSON(http.StatusUnprocessableEntity, validationErrors)
}

func CreateDataModeAction(c *gin.Context) {
    var input createDataModeInput
    if err := c.ShouldBind(&input); err != nil {
        RespondWithValidationError(c, GetValidationErrors(err))
        return
    }
    c.JSON(http.StatusCreated, nil)
}

func main() {
    gin.SetMode(gin.DebugMode)
    r := gin.Default()
    r.POST("/demo", CreateDataModeAction)
    r.Run((":8000"))
}

As you can see, the code is pretty much similar to yours. Now, let's see how to properly test it.

CURL tests

To test out the code, I used two CURL commands: one that must be resolved with a 422 status code and one with 201. The latter command is:

curl -X POST http://127.0.0.1:8000/demo -H 'Content-Type: application/json' -d '{"name":"lorem"}'

Thanks to this, you'll get the created - 201 status code. To test the bad scenario, you should use something like:

curl -X POST http://127.0.0.1:8000/demo -H 'Content-Type: application/json' -d '{"name":""}'

With this statement, you'll get the desired 422 status code.
Sorry, if I didn't add too much explanation but the code is really similar to yours. If you still face some issues, just let me know and I'll update my answer, thanks!

ossan
  • 1,665
  • 4
  • 10
  • 1
    Thanks! I've stripped off all middleware and external libraries in order to reproduce this problem, and in the minimum setup, I'm receiving a correct response. There are two possible answers, either I'm doing something wrong or some of the middlewares or libraries are causing weird side effects. Either way, let's consider this case solved. Thanks for taking the time and helping me out with this. – Andrey Korchak Jun 21 '23 at 08:48