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"`
}