Skip to content
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

Switch to json.Unmarshal instead of json.Decoder.Decode() #174

Merged
merged 3 commits into from
Dec 7, 2022

Conversation

joelrebel
Copy link
Member

This is replaced for a few reasons,

  • The JSON stream decoder blocks forever if the server stops sending data
    midway in the body, see goroutine dump below [0].

  • Switching this to the json.Marshal surfaced issues in implementations where
    multiple JSON blocks were being returned in the response to the client,
    e.g: {"foo": "bar"}{"foo": "baz"}
    they are not caught in tests because json.Decoder.Decode doesn't have a problem with those.

  • As of now this service does not stream JSON and if the need arises,
    the implementation could be switched back to the json.Decoder along
    with a for, select loop on the request context.

[0]. Goroutine hung on json.(*Decoder).Decode

1 @ 0x43b196 0x44abf2 0x6bf129 0x6be71e 0x46ba21
   0x6bf128    net/http.(*http2clientStream).writeRequest+0x9c8
   /opt/hostedtoolcache/go/1.18.5/x64/src/net/http/h2_bundle.go:8037
   0x6be71d    net/http.(*http2clientStream).doRequest+0x1d
   /opt/hostedtoolcache/go/1.18.5/x64/src/net/http/h2_bundle.go:7899

1 @ 0x43b196 0x467afd 0x467add 0x48024c 0x6a9aab 0x6c5be5 0x56ee5f 0x56ea5b
0x56e798 0xb627dc 0xb60de7 0xb64065 0xcd3f85 0xcd30dc 0xcd2ba5 0xd128fe
0x46ba21
   0x467adc    sync.runtime_notifyListWait+0x11c
   /opt/hostedtoolcache/go/1.18.5/x64/src/runtime/sema.go:513
   0x48024b    sync.(*Cond).Wait+0x8b
   /opt/hostedtoolcache/go/1.18.5/x64/src/sync/cond.go:56
   0x6a9aaa    net/http.(*http2pipe).Read+0xea
   /opt/hostedtoolcache/go/1.18.5/x64/src/net/http/h2_bundle.go:3663
   0x6c5be4    net/http.http2transportResponseBody.Read+0x84
   /opt/hostedtoolcache/go/1.18.5/x64/src/net/http/h2_bundle.go:9098
   0x56ee5e    encoding/json.(*Decoder).refill+0x17e
   /opt/hostedtoolcache/go/1.18.5/x64/src/encoding/json/stream.go:165
   0x56ea5a    encoding/json.(*Decoder).readValue+0xba
   /opt/hostedtoolcache/go/1.18.5/x64/src/encoding/json/stream.go:140
   0x56e797    encoding/json.(*Decoder).Decode+0x77
   /opt/hostedtoolcache/go/1.18.5/x64/src/encoding/json/stream.go:63

This is replaced for a few reasons,

 - The JSON stream decoder blocks forever if the server stops sending data
   midway in the body, see goroutine dump below [0].

 - Switching this to the json.Marshal surfaced issues in implementations where
   multiple JSON blocks were being returned in the response to the client,
    e.g: {"foo": "bar"}{"foo": "baz"}
   they are not caught in tests because json.Decoder.Decode doesn't have a problem with those.

 - As of now this service does not stream JSON and if the need arises,
   the implementation could be switched back to the json.Decoder along
   with a for, select loop on the request context.

[0]. Goroutine hung on json.(*Decoder).Decode
```
1 @ 0x43b196 0x44abf2 0x6bf129 0x6be71e 0x46ba21
   0x6bf128    net/http.(*http2clientStream).writeRequest+0x9c8
   /opt/hostedtoolcache/go/1.18.5/x64/src/net/http/h2_bundle.go:8037
   0x6be71d    net/http.(*http2clientStream).doRequest+0x1d
   /opt/hostedtoolcache/go/1.18.5/x64/src/net/http/h2_bundle.go:7899

1 @ 0x43b196 0x467afd 0x467add 0x48024c 0x6a9aab 0x6c5be5 0x56ee5f 0x56ea5b
0x56e798 0xb627dc 0xb60de7 0xb64065 0xcd3f85 0xcd30dc 0xcd2ba5 0xd128fe
0x46ba21
   0x467adc    sync.runtime_notifyListWait+0x11c
   /opt/hostedtoolcache/go/1.18.5/x64/src/runtime/sema.go:513
   0x48024b    sync.(*Cond).Wait+0x8b
   /opt/hostedtoolcache/go/1.18.5/x64/src/sync/cond.go:56
   0x6a9aaa    net/http.(*http2pipe).Read+0xea
   /opt/hostedtoolcache/go/1.18.5/x64/src/net/http/h2_bundle.go:3663
   0x6c5be4    net/http.http2transportResponseBody.Read+0x84
   /opt/hostedtoolcache/go/1.18.5/x64/src/net/http/h2_bundle.go:9098
   0x56ee5e    encoding/json.(*Decoder).refill+0x17e
   /opt/hostedtoolcache/go/1.18.5/x64/src/encoding/json/stream.go:165
   0x56ea5a    encoding/json.(*Decoder).readValue+0xba
   /opt/hostedtoolcache/go/1.18.5/x64/src/encoding/json/stream.go:140
   0x56e797    encoding/json.(*Decoder).Decode+0x77
   /opt/hostedtoolcache/go/1.18.5/x64/src/encoding/json/stream.go:63
```
In order to reduce ambiguity in who writes the response body,
this method now just returns an error if any. and leaves it
to the caller to write the response based on the error.

Earlier this method was writing a JSON response to the body, which led to multiple
situations where the JSON response was written again by the caller. And
so invalid JSON being sent in the JSON body.
@joelrebel joelrebel requested a review from a team as a code owner December 5, 2022 13:45
@codecov-commenter
Copy link

Codecov Report

Merging #174 (4c9b5b6) into main (f120866) will decrease coverage by 1.17%.
The diff coverage is 28.40%.

@@            Coverage Diff             @@
##             main     #174      +/-   ##
==========================================
- Coverage   72.44%   71.27%   -1.18%     
==========================================
  Files          35       35              
  Lines        3321     3380      +59     
==========================================
+ Hits         2406     2409       +3     
- Misses        673      721      +48     
- Partials      242      250       +8     
Flag Coverage Δ
unittests 71.27% <28.40%> (-1.18%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
pkg/api/v1/router.go 83.56% <0.00%> (-0.23%) ⬇️
pkg/api/v1/router_server.go 52.11% <8.33%> (-4.37%) ⬇️
pkg/api/v1/router_server_attributes.go 34.69% <11.11%> (-3.19%) ⬇️
pkg/api/v1/errors.go 76.00% <20.00%> (-9.72%) ⬇️
pkg/api/v1/router_server_components.go 59.71% <26.08%> (-3.55%) ⬇️
pkg/api/v1/requests.go 66.12% <40.00%> (-2.84%) ⬇️
pkg/api/v1/router_firmware_set.go 60.42% <100.00%> (-0.17%) ⬇️
pkg/api/v1/router_responses.go 94.56% <100.00%> (+0.05%) ⬆️
... and 1 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

sfunkhouser
sfunkhouser previously approved these changes Dec 5, 2022
return err
}

if err := json.Unmarshal(data, &se); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this is a change that could easily be regressed, could you add unit tests that ensure that we don't fall back to the previous behavior? I can very easily see that someone is not aware of the Unmarshall/Decode issues and moves back to using a decoder.

@JAORMX JAORMX merged commit eb3dfa8 into metal-toolbox:main Dec 7, 2022
@joelrebel joelrebel deleted the json-response branch February 7, 2023 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants