Home > Software engineering >  Golang Parsing Request body throws, contains unknown field "password"?
Golang Parsing Request body throws, contains unknown field "password"?

Time:10-09

I'm not sure what is causing this, but the reason it throws it is when I call dec.DisallowUnknownFields() it somehow thinks that password isn't a field in the User struct/request body. Albeit, the password JSON is "-". So I thought originally to change the JSON to "password" for struct field Password but that throws an

internal server error:  illegal base64 data at input byte 4

Request Body in Postman

{
    "firstname": "George",
    "lastname": "Costanza",
    "email": "[email protected]",
    "phone": "703-123-4567",
    "password": "abc12"
}

I also tried to change the type of password to string, which also didn't work but arguably would be the wrong solution because we should store passwords as hashes in the DB. So, at this point I've run out of places to turn to as to why this is happening...

models.go

type User struct {
    ID primitive.ObjectID    `json:"_id,omitempty" bson:"_id,omitempty"`
    FirstName      string    `json:"firstname"`
    LastName       string    `json:"lastname"`
    Email          string    `json:"email"`
    Phone          string    `json:"phone"`
    Password       []byte    `json:"-"`
    Created        time.Time `json:"-"`
    Active         bool      `json:"-"`
    Address        Address   `json:"address,omitempty"`
}

Helpers.go

type malformedRequest struct {
    status int
    msg    string
}

func (mr *malformedRequest) Error() string {
    return mr.msg
}

func (app *appInjection) decodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error {
    if r.Header.Get("Content-Type") != "" {
        value, _ := header.ParseValueAndParams(r.Header, "Content-Type")
        if value != "application/json" {
            msg := "Content-Type header is not application/json"
            return &malformedRequest{status: http.StatusUnsupportedMediaType, msg: msg}
        }
    }

    r.Body = http.MaxBytesReader(w, r.Body, 1048576)

    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()
    err := dec.Decode(&dst)

    if err != nil {
        var syntaxError *json.SyntaxError
        var unmarshalTypeError *json.UnmarshalTypeError

        switch {
        case errors.As(err, &syntaxError):
            msg := fmt.Sprintf("Request body contains badly-formed JSON (at position %d)", syntaxError.Offset)
            return &malformedRequest{status: http.StatusBadRequest, msg: msg}

        case errors.Is(err, io.ErrUnexpectedEOF):
            msg := fmt.Sprintf("Request body contains badly-formed JSON")
            return &malformedRequest{status: http.StatusBadRequest, msg: msg}

        case errors.As(err, &unmarshalTypeError):
            msg := fmt.Sprintf("Request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset)
            return &malformedRequest{status: http.StatusBadRequest, msg: msg}

        case strings.HasPrefix(err.Error(), "json: unknown field "):
            fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
            msg := fmt.Sprintf("Request body contains unknown field %s", fieldName)
            return &malformedRequest{status: http.StatusBadRequest, msg: msg}

        case errors.Is(err, io.EOF):
            msg := "Request body must not be empty"
            return &malformedRequest{status: http.StatusBadRequest, msg: msg}

        case err.Error() == "http: request body too large":
            msg := "Request body must not be larger than 1MB"
            return &malformedRequest{status: http.StatusRequestEntityTooLarge, msg: msg}

        default:
            return err
        }
    }

    err = dec.Decode(&struct{}{})
    if err != io.EOF {
        msg := "Request body must only contain a single JSON object"
        return &malformedRequest{status: http.StatusBadRequest, msg: msg}
    }

    return nil
}

Handlers.go

func (app *appInjection) RegisterUser(w http.ResponseWriter, r *http.Request) {

    // Guide addressing headers, syntax error's, and preventing extra data fields
    // https://www.alexedwards.net/blog/how-to-properly-parse-a-json-request-body
    w.Header().Set("Content-Type", "application/json")
    var newUser models.User
    //Parse the form data
    //err := json.NewDecoder(r.Body).Decode(&newUser)
    err := app.decodeJSONBody(w, r, &newUser)
    if err != nil {
        var mr *malformedRequest
        if errors.As(err, &mr) {
            http.Error(w, mr.msg, mr.status)
            //app.clientError(w, mr.status)
        } else {
            log.Println(err.Error())
            //app.clientError(w, http.StatusInternalServerError)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
        }
        return
    }

    //TODO: Validate the form
    //If there is no error and the form is validated, create a new user from http request
    //Insert the new user into the database
    uid, _ := app.user.Insert(
        newUser.FirstName,
        newUser.LastName,
        newUser.Email,
        newUser.Phone,
        string(newUser.Password))

    json.NewEncoder(w).Encode("Record Inserted")
    json.NewEncoder(w).Encode(uid)
}

User.go

func (u *UserFunctions) Insert(firstname, lastname, email, phone, password string) (primitive.ObjectID, error) {
    //Insert user to the database
    userCollection := u.CLIENT.Database("queue").Collection("users")
    var user models.User

    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
    if err != nil {
        object, _ := primitive.ObjectIDFromHex("")
        return object, err
    }
    user.FirstName = firstname
    user.LastName = lastname
    user.Email = email
    user.Phone = phone
    user.Password = hashedPassword
    user.Created = time.Now().UTC()
    user.Active = true

    //Insert the user into the database
    result, err := userCollection.InsertOne(context.TODO(), user)
    if err != nil {
        fmt.Println(err)
    }

    //Check ID of the inserted document
    insertedID := result.InsertedID.(primitive.ObjectID)
    //fmt.Println(insertedID)

    return insertedID, nil
}

When I run the request to sign up a user in Postman it throws the message

Request body contains unknown field "password"

CodePudding user response:

A simple solution would be

var newUser struct {
    models.User
    Password string `json:"password"`
}
//Parse the form data
//err := json.NewDecoder(r.Body).Decode(&newUser)
err := app.decodeJSONBody(w, r, &newUser)

CodePudding user response:

As folks already linked extensively, since JSON cannot contain bytes directly, the Go Json parser expects any []byte objects to be stored in Base64 in the input data.

Your problem, therefore, is not necessarily on the Go end. Your problem is a mismatch with what you told Go to expect - []byte, Base64 encoded - and what you told the client to send - "abc12" - which is not a valid base64 value.

One simple solution for your stated case would be to send a base64 encoded version of the same string:

{
    "firstname": "George",
    "lastname": "Costanza",
    "email": "[email protected]",
    "phone": "703-123-4567",
    "password": "YWJjMTIK"
}

But I'd like to dig just a little bit deeper.

string [...] arguably would be the wrong solution because we should store passwords as hashes in the DB

You might store passwords hashed into bytes in the database, but that's not what's coming int from curl . So to me, your real problem is that you took 2 different kinds of data - user provided password and DB hashed representation - and tried to mash them into the same data space, which has absolutely no value. After all, if you were setting a password, the current hash, if any, is irrelevant, and if you're checking the password, you'd be checking what they inputted.

This is the solution I'd prefer:

type User struct {
    ... ... ...
    HashedPassword       []byte    `json:"-"`
    Password string `json:"password"`

}
  • Related