@cryptotaxi247 / kubo / commits / c10b80444

feat: Gateway.DeserializedResponses config flag (#9789)

Co-authored-by: Marcin Rataj <lidel@lidel.org>

Henrique Dias committed May 30, 2023 at 00:59 UTC c10b804449cd6e8e354a693f3d0c4bc7bd93fbca
10 files changed +233 -40
config/gateway.go
+15 -1
@@ -1,6 +1,9 @@
1 package config
2
3 -const DefaultInlineDNSLink = false
3 +const (
4 + DefaultInlineDNSLink = false
5 + DefaultDeserializedResponses = true
6 +)
7
8 type GatewaySpec struct {
9 // Paths is explicit list of path prefixes that should be handled by
@@ -25,6 +28,11 @@ type GatewaySpec struct {
28 // (FQDN) into a single DNS label in order to interop with wildcard TLS certs
29 // and Origin per CID isolation provided by rules like https://publicsuffix.org
30 InlineDNSLink Flag
31 +
32 + // DeserializedResponses configures this gateway to respond to deserialized
33 + // responses. Disabling this option enables a Trustless Gateway, as per:
34 + // https://specs.ipfs.tech/http-gateways/trustless-gateway/.
35 + DeserializedResponses Flag
36 }
37
38 // Gateway contains options for the HTTP gateway server.
@@ -56,6 +64,12 @@ type Gateway struct {
64 // This flag can be overridden per FQDN in PublicGateways.
65 NoDNSLink bool
66
67 + // DeserializedResponses configures this gateway to respond to deserialized
68 + // requests. Disabling this option enables a Trustless only gateway, as per:
69 + // https://specs.ipfs.tech/http-gateways/trustless-gateway/. This can
70 + // be overridden per FQDN in PublicGateways.
71 + DeserializedResponses Flag
72 +
73 // PublicGateways configures behavior of known public gateways.
74 // Each key is a fully qualified domain name (FQDN).
75 PublicGateways map[string]*GatewaySpec
core/corehttp/gateway.go
+36 -29
@@ -28,22 +28,11 @@ import (
28
29 func GatewayOption(paths ...string) ServeOption {
30 return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
31 - cfg, err := n.Repo.Config()
31 + gwConfig, err := getGatewayConfig(n)
32 if err != nil {
33 return nil, err
34 }
35
36 - headers := make(map[string][]string, len(cfg.Gateway.HTTPHeaders))
37 - for h, v := range cfg.Gateway.HTTPHeaders {
38 - headers[http.CanonicalHeaderKey(h)] = v
39 - }
40 -
41 - gateway.AddAccessControlHeaders(headers)
42 -
43 - gwConfig := gateway.Config{
44 - Headers: headers,
45 - }
46 -
36 gwAPI, err := newGatewayBackend(n)
37 if err != nil {
38 return nil, err
@@ -65,7 +54,7 @@ func GatewayOption(paths ...string) ServeOption {
54
55 func HostnameOption() ServeOption {
56 return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
68 - cfg, err := n.Repo.Config()
57 + gwConfig, err := getGatewayConfig(n)
58 if err != nil {
59 return nil, err
60 }
@@ -75,9 +64,8 @@ func HostnameOption() ServeOption {
64 return nil, err
65 }
66
78 - publicGateways := convertPublicGateways(cfg.Gateway.PublicGateways)
67 childMux := http.NewServeMux()
80 - mux.HandleFunc("/", gateway.WithHostname(childMux, gwAPI, publicGateways, cfg.Gateway.NoDNSLink).ServeHTTP)
68 + mux.HandleFunc("/", gateway.WithHostname(gwConfig, gwAPI, childMux).ServeHTTP)
69 return childMux, nil
70 }
71 }
@@ -212,30 +200,49 @@ var defaultKnownGateways = map[string]*gateway.Specification{
200 "localhost": subdomainGatewaySpec,
201 }
202
215 -func convertPublicGateways(publicGateways map[string]*config.GatewaySpec) map[string]*gateway.Specification {
216 - gws := map[string]*gateway.Specification{}
203 +func getGatewayConfig(n *core.IpfsNode) (gateway.Config, error) {
204 + cfg, err := n.Repo.Config()
205 + if err != nil {
206 + return gateway.Config{}, err
207 + }
208 +
209 + // Parse configuration headers and add the default Access Control Headers.
210 + headers := make(map[string][]string, len(cfg.Gateway.HTTPHeaders))
211 + for h, v := range cfg.Gateway.HTTPHeaders {
212 + headers[http.CanonicalHeaderKey(h)] = v
213 + }
214 + gateway.AddAccessControlHeaders(headers)
215 +
216 + // Initialize gateway configuration, with empty PublicGateways, handled after.
217 + gwCfg := gateway.Config{
218 + Headers: headers,
219 + DeserializedResponses: cfg.Gateway.DeserializedResponses.WithDefault(config.DefaultDeserializedResponses),
220 + NoDNSLink: cfg.Gateway.NoDNSLink,
221 + PublicGateways: map[string]*gateway.Specification{},
222 + }
223
218 - // First, implicit defaults such as subdomain gateway on localhost
224 + // Add default implicit known gateways, such as subdomain gateway on localhost.
225 for hostname, gw := range defaultKnownGateways {
220 - gws[hostname] = gw
226 + gwCfg.PublicGateways[hostname] = gw
227 }
228
223 - // Then apply values from Gateway.PublicGateways, if present in the config
224 - for hostname, gw := range publicGateways {
229 + // Apply values from cfg.Gateway.PublicGateways if they exist.
230 + for hostname, gw := range cfg.Gateway.PublicGateways {
231 if gw == nil {
232 // Remove any implicit defaults, if present. This is useful when one
227 - // wants to disable subdomain gateway on localhost etc.
228 - delete(gws, hostname)
233 + // wants to disable subdomain gateway on localhost, etc.
234 + delete(gwCfg.PublicGateways, hostname)
235 continue
236 }
237
232 - gws[hostname] = &gateway.Specification{
233 - Paths: gw.Paths,
234 - NoDNSLink: gw.NoDNSLink,
235 - UseSubdomains: gw.UseSubdomains,
236 - InlineDNSLink: gw.InlineDNSLink.WithDefault(config.DefaultInlineDNSLink),
238 + gwCfg.PublicGateways[hostname] = &gateway.Specification{
239 + Paths: gw.Paths,
240 + NoDNSLink: gw.NoDNSLink,
241 + UseSubdomains: gw.UseSubdomains,
242 + InlineDNSLink: gw.InlineDNSLink.WithDefault(config.DefaultInlineDNSLink),
243 + DeserializedResponses: gw.DeserializedResponses.WithDefault(gwCfg.DeserializedResponses),
244 }
245 }
246
240 - return gws
247 + return gwCfg, nil
248 }
core/corehttp/gateway_test.go
+40
@@ -14,6 +14,7 @@ import (
14 core "github.com/ipfs/kubo/core"
15 "github.com/ipfs/kubo/core/coreapi"
16 repo "github.com/ipfs/kubo/repo"
17 + "github.com/stretchr/testify/assert"
18
19 iface "github.com/ipfs/boxo/coreiface"
20 nsopts "github.com/ipfs/boxo/coreiface/options/namesys"
@@ -173,3 +174,42 @@ func TestVersion(t *testing.T) {
174 t.Fatalf("response doesn't contain protocol version:\n%s", s)
175 }
176 }
177 +
178 +func TestDeserializedResponsesInheritance(t *testing.T) {
179 + for _, testCase := range []struct {
180 + globalSetting config.Flag
181 + gatewaySetting config.Flag
182 + expectedGatewaySetting bool
183 + }{
184 + {config.True, config.Default, true},
185 + {config.False, config.Default, false},
186 + {config.False, config.True, true},
187 + {config.True, config.False, false},
188 + } {
189 + c := config.Config{
190 + Identity: config.Identity{
191 + PeerID: "QmTFauExutTsy4XP6JbMFcw2Wa9645HJt2bTqL6qYDCKfe", // required by offline node
192 + },
193 + Gateway: config.Gateway{
194 + DeserializedResponses: testCase.globalSetting,
195 + PublicGateways: map[string]*config.GatewaySpec{
196 + "example.com": {
197 + DeserializedResponses: testCase.gatewaySetting,
198 + },
199 + },
200 + },
201 + }
202 + r := &repo.Mock{
203 + C: c,
204 + D: syncds.MutexWrap(datastore.NewMapDatastore()),
205 + }
206 + n, err := core.NewNode(context.Background(), &core.BuildCfg{Repo: r})
207 + assert.NoError(t, err)
208 +
209 + gwCfg, err := getGatewayConfig(n)
210 + assert.NoError(t, err)
211 +
212 + assert.Contains(t, gwCfg.PublicGateways, "example.com")
213 + assert.Equal(t, testCase.expectedGatewaySetting, gwCfg.PublicGateways["example.com"].DeserializedResponses)
214 + }
215 +}
docs/changelogs/v0.21.md
+27
@@ -7,6 +7,7 @@
7 - [Overview](#overview)
8 - [🔦 Highlights](#-highlights)
9 - [Saving previously seen nodes for later bootstrapping](#saving-previously-seen-nodes-for-later-bootstrapping)
10 + - [`Gateway.DeserializedResponses` config flag](#gatewaydeserializedresponses-config-flag)
11 - [📝 Changelog](#-changelog)
12 - [👨‍👩‍👧‍👦 Contributors](#-contributors)
13
@@ -29,6 +30,32 @@ enabled.
30 With this update, the same level of robustness is applied to peers that lack
31 mDNS peers and solely rely on the public DHT.
32
33 +
34 +#### `Gateway.DeserializedResponses` config flag
35 +
36 +This release introduces the
37 +[`Gateway.DeserializedResponses`](https://github.com/ipfs/kubo/blob/master/docs/config.md#gatewaydeserializedresponses)
38 +configuration flag.
39 +
40 +With this flag, one can explicitly configure whether the gateway responds to
41 +deserialized requests or not. By default, this flag is enabled.
42 +
43 +Disabling deserialized responses allows the
44 +gateway to operate
45 +as a [Trustless Gateway](https://specs.ipfs.tech/http-gateways/trustless-gateway/)
46 +limited to three [verifiable](https://docs.ipfs.tech/reference/http/gateway/#trustless-verifiable-retrieval)
47 +response types:
48 +[application/vnd.ipld.raw](https://www.iana.org/assignments/media-types/application/vnd.ipld.raw),
49 +[application/vnd.ipld.car](https://www.iana.org/assignments/media-types/application/vnd.ipld.car),
50 +and [application/vnd.ipfs.ipns-record](https://www.iana.org/assignments/media-types/application/vnd.ipfs.ipns-record).
51 +
52 +With deserialized responses disabled, the Kubo gateway can serve as a block
53 +backend for other software (like
54 +[bifrost-gateway](https://github.com/ipfs/bifrost-gateway#readme),
55 +[IPFS in Chromium](https://github.com/little-bear-labs/ipfs-chromium/blob/main/README.md)
56 +etc) without the usual risks associated with hosting deserialized data behind
57 +third-party CIDs.
58 +
59 ### 📝 Changelog
60
61 ### 👨‍👩‍👧‍👦 Contributors
docs/config.md
+26 -4
@@ -50,6 +50,7 @@ config file at runtime.
50 - [`Gateway`](#gateway)
51 - [`Gateway.NoFetch`](#gatewaynofetch)
52 - [`Gateway.NoDNSLink`](#gatewaynodnslink)
53 + - [`Gateway.DeserializedResponses`](#gatewaydeserializedresponses)
54 - [`Gateway.HTTPHeaders`](#gatewayhttpheaders)
55 - [`Gateway.RootRedirect`](#gatewayrootredirect)
56 - [`Gateway.FastDirIndexThreshold`](#gatewayfastdirindexthreshold)
@@ -60,6 +61,7 @@ config file at runtime.
61 - [`Gateway.PublicGateways: UseSubdomains`](#gatewaypublicgateways-usesubdomains)
62 - [`Gateway.PublicGateways: NoDNSLink`](#gatewaypublicgateways-nodnslink)
63 - [`Gateway.PublicGateways: InlineDNSLink`](#gatewaypublicgateways-inlinednslink)
64 + - [`Gateway.PublicGateways: DeserializedResponses`](#gatewaypublicgateways-deserializedresponses)
65 - [Implicit defaults of `Gateway.PublicGateways`](#implicit-defaults-of-gatewaypublicgateways)
66 - [`Gateway` recipes](#gateway-recipes)
67 - [`Identity`](#identity)
@@ -236,7 +238,7 @@ documented in `ipfs config profile --help`.
238 smaller than several gigabytes. If you run IPFS with `--enable-gc`, you plan on storing very little data in
239 your IPFS node, and disk usage is more critical than performance, consider using
240 `flatfs`.
239 - - This datastore uses up to several gigabytes of memory.
241 + - This datastore uses up to several gigabytes of memory.
242 - Good for medium-size datastores, but may run into performance issues if your dataset is bigger than a terabyte.
243 - The current implementation is based on old badger 1.x which is no longer supported by the upstream team.
244
@@ -646,6 +648,16 @@ Default: `false`
648
649 Type: `bool`
650
651 +#### `Gateway.DeserializedResponses`
652 +
653 +An optional flag to explicitly configure whether this gateway responds to deserialized
654 +requests, or not. By default, it is enabled. When disabling this option, the gateway
655 +operates as a Trustless Gateway only: https://specs.ipfs.tech/http-gateways/trustless-gateway/.
656 +
657 +Default: `true`
658 +
659 +Type: `flag`
660 +
661 ### `Gateway.HTTPHeaders`
662
663 Headers to set on gateway responses.
@@ -790,6 +802,16 @@ Default: `false`
802
803 Type: `flag`
804
805 +#### `Gateway.PublicGateways: DeserializedResponses`
806 +
807 +An optional flag to explicitly configure whether this gateway responds to deserialized
808 +requests, or not. By default, it is enabled. When disabling this option, the gateway
809 +operates as a Trustless Gateway only: https://specs.ipfs.tech/http-gateways/trustless-gateway/.
810 +
811 +Default: same as global `Gateway.DeserializedResponses`
812 +
813 +Type: `flag`
814 +
815 #### Implicit defaults of `Gateway.PublicGateways`
816
817 Default entries for `localhost` hostname and loopback IPs are always present.
@@ -895,7 +917,7 @@ Type: `string` (base64 encoded)
917
918 ## `Internal`
919
898 -This section includes internal knobs for various subsystems to allow advanced users with big or private infrastructures to fine-tune some behaviors without the need to recompile Kubo.
920 +This section includes internal knobs for various subsystems to allow advanced users with big or private infrastructures to fine-tune some behaviors without the need to recompile Kubo.
921
922 **Be aware that making informed change here requires in-depth knowledge and most users should leave these untouched. All knobs listed here are subject to breaking changes between versions.**
923
@@ -971,7 +993,7 @@ Type: `optionalInteger` (byte count, `null` means default which is 1MB)
993 ### `Internal.Bitswap.ProviderSearchDelay`
994
995 This parameter determines how long to wait before looking for providers outside of bitswap.
974 -Other routing systems like the DHT are able to provide results in less than a second, so lowering
996 +Other routing systems like the DHT are able to provide results in less than a second, so lowering
997 this number will allow faster peers lookups in some cases.
998
999 Type: `optionalDuration` (`null` means default which is 1s)
@@ -1552,7 +1574,7 @@ another node, even if this other node is on a different network. This may
1574 trigger netscan alerts on some hosting providers or cause strain in some setups.
1575
1576 The `server` configuration profile fills up this list with sensible defaults,
1555 -preventing dials to all non-routable IP addresses (e.g., `/ip4/192.168.0.0/ipcidr/16`,
1577 +preventing dials to all non-routable IP addresses (e.g., `/ip4/192.168.0.0/ipcidr/16`,
1578 which is the multiaddress representation of `192.168.0.0/16`) but you should always
1579 check settings against your own network and/or hosting provider.
1580
docs/examples/kubo-as-a-library/go.mod
+1 -1
@@ -7,7 +7,7 @@ go 1.18
7 replace github.com/ipfs/kubo => ./../../..
8
9 require (
10 - github.com/ipfs/boxo v0.8.2-0.20230525115135-a8533c998f49
10 + github.com/ipfs/boxo v0.8.2-0.20230529214945-86cdb2485dad
11 github.com/ipfs/kubo v0.0.0-00010101000000-000000000000
12 github.com/libp2p/go-libp2p v0.27.3
13 github.com/multiformats/go-multiaddr v0.9.0
docs/examples/kubo-as-a-library/go.sum
+2 -2
@@ -321,8 +321,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
321 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
322 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
323 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
324 -github.com/ipfs/boxo v0.8.2-0.20230525115135-a8533c998f49 h1:hi2x0dCINl9fHIV6YM+IH+Bah45pRAFekjM5MMKWJO4=
325 -github.com/ipfs/boxo v0.8.2-0.20230525115135-a8533c998f49/go.mod h1:Ej2r08Z4VIaFKqY08UXMNhwcLf6VekHhK8c+KqA1B9Y=
324 +github.com/ipfs/boxo v0.8.2-0.20230529214945-86cdb2485dad h1:2vkMvvVa5f9fWzts7OcJL6ZS0QaKCcEeOV6I+doPMo0=
325 +github.com/ipfs/boxo v0.8.2-0.20230529214945-86cdb2485dad/go.mod h1:Ej2r08Z4VIaFKqY08UXMNhwcLf6VekHhK8c+KqA1B9Y=
326 github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
327 github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
328 github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY=
go.mod
+1 -1
@@ -16,7 +16,7 @@ require (
16 github.com/gogo/protobuf v1.3.2
17 github.com/google/uuid v1.3.0
18 github.com/hashicorp/go-multierror v1.1.1
19 - github.com/ipfs/boxo v0.8.2-0.20230525115135-a8533c998f49
19 + github.com/ipfs/boxo v0.8.2-0.20230529214945-86cdb2485dad
20 github.com/ipfs/go-block-format v0.1.2
21 github.com/ipfs/go-cid v0.4.1
22 github.com/ipfs/go-cidutil v0.1.0
go.sum
+2 -2
@@ -356,8 +356,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
356 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
357 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
358 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
359 -github.com/ipfs/boxo v0.8.2-0.20230525115135-a8533c998f49 h1:hi2x0dCINl9fHIV6YM+IH+Bah45pRAFekjM5MMKWJO4=
360 -github.com/ipfs/boxo v0.8.2-0.20230525115135-a8533c998f49/go.mod h1:Ej2r08Z4VIaFKqY08UXMNhwcLf6VekHhK8c+KqA1B9Y=
359 +github.com/ipfs/boxo v0.8.2-0.20230529214945-86cdb2485dad h1:2vkMvvVa5f9fWzts7OcJL6ZS0QaKCcEeOV6I+doPMo0=
360 +github.com/ipfs/boxo v0.8.2-0.20230529214945-86cdb2485dad/go.mod h1:Ej2r08Z4VIaFKqY08UXMNhwcLf6VekHhK8c+KqA1B9Y=
361 github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
362 github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
363 github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY=
test/cli/gateway_test.go
+83
@@ -513,4 +513,87 @@ func TestGateway(t *testing.T) {
513 })
514 })
515 })
516 +
517 + t.Run("DeserializedResponses", func(t *testing.T) {
518 + type testCase struct {
519 + globalValue config.Flag
520 + gatewayValue config.Flag
521 + deserializedGlobalStatusCode int
522 + deserializedGatewayStaticCode int
523 + message string
524 + }
525 +
526 + setHost := func(r *http.Request) {
527 + r.Host = "example.com"
528 + }
529 +
530 + withAccept := func(accept string) func(r *http.Request) {
531 + return func(r *http.Request) {
532 + r.Header.Set("Accept", accept)
533 + }
534 + }
535 +
536 + withHostAndAccept := func(accept string) func(r *http.Request) {
537 + return func(r *http.Request) {
538 + setHost(r)
539 + withAccept(accept)(r)
540 + }
541 + }
542 +
543 + makeTest := func(test *testCase) func(t *testing.T) {
544 + return func(t *testing.T) {
545 + t.Parallel()
546 +
547 + node := harness.NewT(t).NewNode().Init()
548 + node.UpdateConfig(func(cfg *config.Config) {
549 + cfg.Gateway.DeserializedResponses = test.globalValue
550 + cfg.Gateway.PublicGateways = map[string]*config.GatewaySpec{
551 + "example.com": {
552 + Paths: []string{"/ipfs", "/ipns"},
553 + DeserializedResponses: test.gatewayValue,
554 + },
555 + }
556 + })
557 + node.StartDaemon()
558 +
559 + cidFoo := node.IPFSAddStr("foo")
560 + client := node.GatewayClient()
561 +
562 + deserializedPath := "/ipfs/" + cidFoo
563 +
564 + blockPath := deserializedPath + "?format=raw"
565 + carPath := deserializedPath + "?format=car"
566 +
567 + // Global Check (Gateway.DeserializedResponses)
568 + assert.Equal(t, http.StatusOK, client.Get(blockPath).StatusCode)
569 + assert.Equal(t, http.StatusOK, client.Get(deserializedPath, withAccept("application/vnd.ipld.raw")).StatusCode)
570 +
571 + assert.Equal(t, http.StatusOK, client.Get(carPath).StatusCode)
572 + assert.Equal(t, http.StatusOK, client.Get(deserializedPath, withAccept("application/vnd.ipld.car")).StatusCode)
573 +
574 + assert.Equal(t, test.deserializedGlobalStatusCode, client.Get(deserializedPath).StatusCode)
575 + assert.Equal(t, test.deserializedGlobalStatusCode, client.Get(deserializedPath, withAccept("application/json")).StatusCode)
576 +
577 + // Public Gateway (example.com) Check (Gateway.PublicGateways[example.com].DeserializedResponses)
578 + assert.Equal(t, http.StatusOK, client.Get(blockPath, setHost).StatusCode)
579 + assert.Equal(t, http.StatusOK, client.Get(deserializedPath, withHostAndAccept("application/vnd.ipld.raw")).StatusCode)
580 +
581 + assert.Equal(t, http.StatusOK, client.Get(carPath, setHost).StatusCode)
582 + assert.Equal(t, http.StatusOK, client.Get(deserializedPath, withHostAndAccept("application/vnd.ipld.car")).StatusCode)
583 +
584 + assert.Equal(t, test.deserializedGatewayStaticCode, client.Get(deserializedPath, setHost).StatusCode)
585 + assert.Equal(t, test.deserializedGatewayStaticCode, client.Get(deserializedPath, withHostAndAccept("application/json")).StatusCode)
586 +
587 + }
588 + }
589 +
590 + for _, test := range []*testCase{
591 + {config.True, config.Default, http.StatusOK, http.StatusOK, "when Gateway.DeserializedResponses is globally enabled, leaving implicit default for Gateway.PublicGateways[example.com] should inherit the global setting (enabled)"},
592 + {config.False, config.Default, http.StatusNotAcceptable, http.StatusNotAcceptable, "when Gateway.DeserializedResponses is globally disabled, leaving implicit default on Gateway.PublicGateways[example.com] should inherit the global setting (disabled)"},
593 + {config.False, config.True, http.StatusNotAcceptable, http.StatusOK, "when Gateway.DeserializedResponses is globally disabled, explicitly enabling on Gateway.PublicGateways[example.com] should override global (enabled)"},
594 + {config.True, config.False, http.StatusOK, http.StatusNotAcceptable, "when Gateway.DeserializedResponses is globally enabled, explicitly disabling on Gateway.PublicGateways[example.com] should override global (disabled)"},
595 + } {
596 + t.Run(test.message, makeTest(test))
597 + }
598 + })
599 }