androidgoretrofitgo-gingrpc-go

POST to Gin fails when it comes from my Android app, but succeeds when from Postman


I'm not quite sure which side is the cause of the issue, but it seems that the request gets received by Gin but doesn't allow the request from the android app. The endpoint I'm trying to POST to is related to a monolith model which has a type field, and that type field "tells" Gin which are the required fields(my workflow is kinda complicated for that part of my system). On the android side, I have a counterpart model for that monolith model as I use it for some stuff in the app. Previously, I only need to get the data related to monolith model from the server. Now, I'm implementing the POST for it. The android copy of the server model looks like this:

@Entity(tableName = "thing", indices = [Index(value = ["id"], unique = true)])
data class Thing(
    @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "id") var id: String,
    @SerializedName("CreatedAt") @ColumnInfo(name = "created_at") var created_at: String,
    @ColumnInfo(name = "name") var name: String,
    @ColumnInfo(name = "type_id") var type_id: String,
    @ColumnInfo("type") var type: ThingType? = null,
    @ColumnInfo(
        name = "some_field_1",
        defaultValue = "0"
    ) var some_field_1: Float = 0.0f,
    @ColumnInfo(
        name = "some_field_2",
        defaultValue = "0"
    ) var some_field_2: Float = 0.0f,
    @ColumnInfo(
        name = "some_field_3",
        defaultValue = "0"
    ) var some_field_3: Float = 0.0f,
    @ColumnInfo(
        name = "some_field_4",
        defaultValue = "0"
    ) var some_field_4: Float = 0.0f,
    @ColumnInfo(
        name = "some_field_5",
        defaultValue = "0"
    ) var some_field_5: Float = 0.0f,
    @ColumnInfo(
        name = "parent_id",
        defaultValue = "0"
    ) var parent_id: String
)

Then this is a snippet of the monolith server model:

type Thing struct {
    gorm.Model
    ID            string    `gorm:"primarykey; size:40;" json:"id"`
    Name          string    `json:"name"`
    TypeId        string    `gorm:"size:40; index; not null" json:"type_id"`
    Type          Type      `gorm:"foreignKey:TypeId; constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"type"`
    SomeField1    float32   `json:"some_field_1"`
    SomeField2    float32   `json:"some_field_2"`
    SomeField3    float32   `json:"some_field_3"`
    SomeField4    float32   `json:"some_field_4"`
    SomeField5    float32   `json:"some_field_5"`
    ParentId      float32   `json:"parent_id"`
}

Then I POST a Thing using Retrofit like this:

@POST("/thing_info")
suspend fun postThingInfo(
    @HeaderMap headers: Map<String, String>,
    @Body data: Thing
): Response<Thing>

val thing =
    Thing(
        "",
        "",
        name = binding.txtName.text.toString(),
        type_id = typeId,
        some_field_1 = binding.txtSomeField1.text.toString()
            .toFloat(),
        some_field_2 = binding.txtSomeField2.text.toString()
            .toFloat(),
        some_field_3 = binding.txtSomeField3.text.toString()
            .toFloat(),
        parent_id = parentId,
    )

//then the usual Retrofit boilerplate

With those code, I only see that the request gets a 201 but no error even though I return something for every error object that appears in the process.

Here's the golang code btw:

ginGine.POST("/thing_info", func(ctx *gin.Context) {
    errMsg := clnHelpers.ValidateAccessToken(ctx.GetHeader("access_token"))

    if errMsg != "" {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": errMsg,
        })
        return
    }

    var thing_info models.Thing

    err := ctx.ShouldBind(&thing_info)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    data := &pb.ThingInfo{
        Name:                        thing_info.Name,
        TypeId:                      thing_info.TypeId,
        //other field association
    }

    res, err := srvClient.service.CreateThingInfo(ctx, &pb.CreateThingInfoRequest{
        ThingInfo: data,
    })

    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    ctx.JSON(http.StatusCreated, gin.H{
        "thing_info": res.ThingInfo,
    })
})

Solution

  • Finally found the answer to this question, and I think it's related to gorm. So in my actual android model, I added created_at to it as I need it for some sorting on the android side. Because it was a field automatically added by gorm, I thought I don't really need to supply a value to it when POSTing a Thing, I don't even need to pass it on Postman. Initially, I was just passing an empty string to it, but it didn't like it. Then I thought having

    var thing_info models.ThingInfo
    
    err := ctx.ShouldBind(&thing_info)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }
    

    in my code would give me the error back like how it used to be for all the other errors I've had so far. But it didn't, somehow that previous error isn't getting returned to me. Then I inserted a good ol'

    fmt.Printf("thing_info bind err: %v\n", err)
    

    inside that if statement, and got an error finally(the irony of being happy to find an error). It seems like, again, even though created_at was a gorm added field, it was trying to convert the empty string I was passing from the android app to a date and time. After some testing, it seems like passing a string that looks like 2023-08-07T4:46:00Z is acceptable, and fortunately, gorm seems to disregard that value and is putting the actual creation date-time of the record.