Skip to content

Fix Bindings for empty fields #1578

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed

Conversation

mariaefi29
Copy link

@mariaefi29 mariaefi29 commented May 22, 2020

This PR is related to the already discussed issue (#1521).

Without that fix empty fields have default values during binding that does not fail. This situation makes it difficult to validate requests later.

For instance, I had an empty required bool field in the request. It turns into false, binding works without error and the filed passes subsequent validation. This is not a valid behaviour. We would like bindings to fail in such cases like json.Umsarshal. If you try to unmarshal empty field into a bool, it will fail with error.

PR should be tagged as v5.

@stale
Copy link

stale bot commented Nov 7, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Nov 7, 2020
@lammel lammel removed the wontfix label Dec 13, 2020
@lammel lammel linked an issue Dec 15, 2020 that may be closed by this pull request
3 tasks
@stale
Copy link

stale bot commented Jun 26, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed within a month if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale Marked as stale for auto-closing label Jun 26, 2021
@stale stale bot closed this Jan 9, 2022
@aldas aldas reopened this Jan 9, 2022
@stale stale bot removed the stale Marked as stale for auto-closing label Jan 9, 2022
@codecov
Copy link

codecov bot commented Jan 9, 2022

Codecov Report

Merging #1578 (5c5bf13) into master (43e32ba) will increase coverage by 7.02%.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1578      +/-   ##
==========================================
+ Coverage   84.96%   91.98%   +7.02%     
==========================================
  Files          28       34       +6     
  Lines        2168     3793    +1625     
==========================================
+ Hits         1842     3489    +1647     
+ Misses        211      201      -10     
+ Partials      115      103      -12     
Impacted Files Coverage Δ
bind.go 93.50% <ø> (+7.79%) ⬆️
middleware/middleware.go 95.34% <0.00%> (-4.66%) ⬇️
binder.go 100.00% <0.00%> (ø)
middleware/rate_limiter.go 100.00% <0.00%> (ø)
middleware/timeout.go 100.00% <0.00%> (ø)
middleware/decompress.go 96.29% <0.00%> (ø)
json.go 100.00% <0.00%> (ø)
middleware/request_logger.go 97.59% <0.00%> (ø)
middleware/slash.go 92.00% <0.00%> (+0.51%) ⬆️
router.go 98.00% <0.00%> (+0.84%) ⬆️
... and 16 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 43e32ba...5c5bf13. Read the comment docs.

@aldas
Copy link
Contributor

aldas commented Jan 9, 2022

@lammel I think we should merge this. Bool having true/false is one thing but overwriting values with da type default value makes chaining different binding functions impossible - as it will overwrite previously successfully bound values. See #1521 (comment)

@lammel
Copy link
Contributor

lammel commented Jan 9, 2022

As this change existing behavior I'd prefer merge for v5. There might be some implementations that rely on the current (mis)behaviour and we would suddenly break those with a new tag.
On the other hand the current chaining restriction exists for quite some time, so I'd vote for merging for v5

@aldas
Copy link
Contributor

aldas commented Jan 9, 2022

I have to admit that I am wrong here. I did some additional tests and debugged bind. Part where I state that each Bind overwrites previous value is half true. Field is overwritten only if that key exists in params/query/form value map. In that case we assign first assign default value that field and try to parse actual value. So we "default" only when that value is empty in that map - which is OK.

@aldas aldas closed this Jan 9, 2022
@mariaefi29
Copy link
Author

So we "default" only when that value is empty in that map - which is OK.

Dear @aldas,

I didn't quite catch why this is OK. Could you please kindly help me understand? In the issue #1521 I have stated that it would be great to validate if the field was present or not and "defaulting" missing fields makes it impossible.

@aldas
Copy link
Contributor

aldas commented Jan 10, 2022

This is because you should end up in set*Field method when you params/query/form had that key provided.

Example:

func main() {
	e := echo.New()

	e.GET("/", func(c echo.Context) error {
		var params struct {
			ID int64 `query:"id" form:"id"`
		}
		params.ID = 100
		if err := c.Bind(&params); err != nil {
			return err
		}
		return c.String(http.StatusOK, fmt.Sprintf("%+v\n", params))
	})

	log.Fatal(e.Start(":8080"))
}

Output for some test cases:

when no query param is provided - field is not bound

x@x:~/code/$ curl http://localhost:8080/
{ID:100}

when value is provide - field is bound

x@x:~/code/$ curl http://localhost:8080/?id=1
{ID:1}

when empty value is provided - field is bound to default value

x@x:~/code/$ curl http://localhost:8080/?id=
{ID:0}

There is a quite significant difference between providing value (even if empty) and not providing (first means that you have taken time to provide that field in query/form values - if even if empty it means that empty value has been sent intentionally). If we would ignore "empty" values then you would not be able to send explicit empty values

@aldas
Copy link
Contributor

aldas commented Jan 10, 2022

Same with bool

func main() {
	e := echo.New()

	e.GET("/", func(c echo.Context) error {
		var params struct {
			ID   int64 `query:"id" form:"id"`
			Bool bool  `query:"b" form:"b"`
		}
		params.ID = 100
		params.Bool = true
		if err := c.Bind(&params); err != nil {
			return err
		}
		return c.String(http.StatusOK, fmt.Sprintf("%+v\n", params))
	})

	log.Fatal(e.Start(":8080"))
}

Output:

x@x:~/code/$ curl http://localhost:8080/
{ID:100 Bool:true}
x@x:~/code/$ curl http://localhost:8080/?b=
{ID:100 Bool:false}
x@x:~/code/$ curl http://localhost:8080/?b=true
{ID:100 Bool:true}
x@x:~/code/$ curl http://localhost:8080/?b=false
{ID:100 Bool:false}

@aldas
Copy link
Contributor

aldas commented Jan 10, 2022

and after scrolling up and checking what was the original problem

We have this similar behaviour during json.Unmarshal: it cannot unmarshal empty string into a bool variable. It throws an explicit error.

now I have to agree with you. Probably treating "empty" as default value is not appropriate when json unmarshaller does not allow it.

I personally considered it from the use-case when you chain multiple binding methods and if it overwrites or not and not in perspective if "empty" should be allowed to be fail at conversion from string to type

x@x:~/code/$ curl http://localhost:8080/?b=
{ID:100 Bool:false}

@lammel should this be error when empty bool fails with json?

@mariaefi29
Copy link
Author

mariaefi29 commented Jan 10, 2022

In that case I would expect

x@x:~/code/$ curl http://localhost:8080/?b=
{ID:100}

And if validating further with c.Validate(req) (where c is echo.Context) it would fail with the error and that was exactly what was needed) there is no other way to properly validate the request on missing fields if we "defaulting them". This PR was fixing the issue.

@aldas
Copy link
Contributor

aldas commented Jan 10, 2022

{ID:100} is not possible - bool has always value. Unless params is created with pointer to pool

func main() {
	e := echo.New()

	e.GET("/", func(c echo.Context) error {
		var params struct {
			ID   int64 `query:"id" form:"id"`
			Bool *bool `query:"b" form:"b"`
		}
		params.ID = 100
		if err := c.Bind(&params); err != nil {
			return err
		}
		return c.String(http.StatusOK, fmt.Sprintf("%+v\n", params))
	})

	log.Fatal(e.Start(":8080"))
}

Output

x@x:~/code/$ curl http://localhost:8080/
{ID:100 Bool:<nil>}

@aldas
Copy link
Contributor

aldas commented Jan 10, 2022

This is example how empty bool fails with json

func main() {
	e := echo.New()

	e.POST("/", func(c echo.Context) error {
		var params struct {
			ID   int64 `query:"id" form:"id" json:"id"`
			Bool bool  `query:"b" form:"b" json:"b"`
		}
		params.ID = 100
		params.Bool = true
		if err := c.Bind(&params); err != nil {
			return err
		}
		return c.String(http.StatusOK, fmt.Sprintf("%+v\n", params))
	})

	log.Fatal(e.Start(":8080"))
}
x@x:~/code/$ curl -H 'Content-Type: application/json' http://localhost:8080/?b=true -d '{}'
{ID:100 Bool:true}
x@x:~/code/$ curl -H 'Content-Type: application/json' http://localhost:8080/ -d '{}'
{ID:100 Bool:true}
x@x:~/code/$ curl -H 'Content-Type: application/json' http://localhost:8080/?b=false -d '{}'
{ID:100 Bool:true}
x@x:~/code/$ curl -H 'Content-Type: application/json' http://localhost:8080/?b=false -d ''
{ID:100 Bool:true}
x@x:~/code/$ curl -H 'Content-Type: application/json' http://localhost:8080/ -d '{"b":false}'
{ID:100 Bool:false}
x@x:~/code/$ curl -H 'Content-Type: application/json' http://localhost:8080/ -d '{"b":""}'
{"message":"Unmarshal type error: expected=bool, got=string, field=b, offset=7"}

@mariaefi29
Copy link
Author

Yes, I am sorry, I think I lost some context over the time.

You are right, what I meant was if we remove the defaulting in the code, binding will fail if the value is not present.

*bool is what I ended up using for a proper validation.

I think I started having second thoughts about defaulting. If we remove the defaulting, all the fields will be automatically "required", right? and this is not what we want either.

@aldas
Copy link
Contributor

aldas commented Jan 10, 2022

If we would remove defaulting empty values we would only affect use cases like http://localhost:8080/?b= (make them fail) and cases like http://localhost:8080/ are not affected (still pass).

This is because for first URL Binder gets empty string "" for key b and starts to converting it from string to actual type. For second URL it does not get or do anything because that key does not exist query parameters map.

@mariaefi29
Copy link
Author

mariaefi29 commented Jan 10, 2022

Oh, yes, thank you for pointing that out! Hence, I think we should remove the defaulting. What do you think?

@aldas
Copy link
Contributor

aldas commented Jan 10, 2022

I think for for ints/floats/bool empty value should have same result as it would have with JSON. I think it is expected behavior to have an error when you try to convert "" to int/float/bool.

@aldas
Copy link
Contributor

aldas commented Jan 10, 2022

@lammel could you check and validate. I have had bad takes in this topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Empty string turns into "false" bool during binding
3 participants