Skip to content

Commit 8642e26

Browse files
committed
fix: prefer JSON in default Accept negotiation
1 parent 773b076 commit 8642e26

12 files changed

Lines changed: 99 additions & 40 deletions

docs/design/003-content-types-and-encodings.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,23 @@ text, structured values, and raw binary.
212212
Restish should generate a useful `Accept` header based on registered content
213213
types and their quality values.
214214

215-
That ordering is user-visible and can affect what servers return. If CBOR is
216-
preferred over JSON, users may observe CBOR responses from APIs that support
217-
both.
215+
That ordering is user-visible and can affect what servers return. The default
216+
ordering should favor broadly interoperable, text-friendly structured formats
217+
before compact binary structured formats such as CBOR, MessagePack, and Ion.
218+
JSON and vendor JSON should be the strongest default preference; NDJSON and
219+
YAML should remain preferred over binary structured formats without tying JSON.
220+
Binary formats remain first-class decoding and output targets, but binary
221+
response negotiation should be opt-in through an explicit `Accept` header, an
222+
API profile, or an endpoint that only returns that format. Separately, users can
223+
still emit a binary structured document from any decoded response with an
224+
explicit output format such as `-o cbor`.
225+
226+
This is deliberately independent of whether stdout is a TTY. Varying
227+
server-side negotiation based on redirection would make the same command ask
228+
for different representations depending on the shell plumbing around it. The
229+
more predictable model is: default negotiation asks for the representation most
230+
likely to work everywhere; output selection decides whether Restish preserves
231+
the original body bytes or decodes and re-renders them.
218232

219233
The negotiation algorithm is:
220234

@@ -235,10 +249,12 @@ exact list may grow as built-in aliases are added, but the quality order and
235249
deduplication rules remain stable:
236250

237251
```text
238-
application/cbor;q=0.9, application/msgpack;q=0.8, application/x-msgpack;q=0.8, application/vnd.msgpack;q=0.8, application/ion;q=0.8, text/ion;q=0.8, application/json;q=0.5, application/x-ndjson;q=0.5, application/ndjson;q=0.5, application/jsonl;q=0.5, application/jsonlines;q=0.5, application/yaml;q=0.5, application/x-yaml;q=0.5, text/yaml;q=0.5, text/x-yaml;q=0.5, application/x-www-form-urlencoded;q=0.3, multipart/form-data;q=0.3, text/event-stream;q=0.2, text/plain;q=0.2, text/*;q=0.2, application/octet-stream;q=0.1, */*;q=0.1
252+
application/json;q=0.9, application/x-ndjson;q=0.8, application/ndjson;q=0.8, application/jsonl;q=0.8, application/jsonlines;q=0.8, application/yaml;q=0.8, application/x-yaml;q=0.8, text/yaml;q=0.8, text/x-yaml;q=0.8, application/cbor;q=0.6, application/msgpack;q=0.6, application/x-msgpack;q=0.6, application/vnd.msgpack;q=0.6, application/ion;q=0.6, text/ion;q=0.6, application/x-www-form-urlencoded;q=0.3, multipart/form-data;q=0.3, application/xml;q=0.2, text/xml;q=0.2, text/event-stream;q=0.2, text/plain;q=0.2, text/*;q=0.2, application/octet-stream;q=0.1, */*;q=0.1
239253
```
240254

241-
Quality ordering should be stable and deliberate.
255+
Quality ordering should be stable and deliberate. It is part of the CLI's
256+
product contract because generated commands, generic requests, verbose output,
257+
and server behavior all expose it.
242258

243259
Restish should not advertise suffix forms like `application/*+json` unless the
244260
runtime has an explicit reason to do so. Accept generation is based on concrete

docs/design/009-response-normalization-and-output.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,14 @@ after HTTP content-encoding decompression. This keeps shell redirection aligned
217217
with the common file-saving mental model, even for structured binary formats
218218
such as CBOR.
219219

220+
This raw-byte rule is separate from content negotiation. Restish's generated
221+
default `Accept` header should still prefer JSON and other text-friendly
222+
structured formats over CBOR, MessagePack, and Ion so the same command asks for
223+
the same broadly interoperable representation whether stdout is a terminal,
224+
pipe, or file. If a server returns CBOR because the user requested it, an API
225+
profile requested it, or the endpoint only supports it, redirected unfiltered
226+
output still preserves those response bytes.
227+
220228
The practical rule is:
221229

222230
- if Restish is still outputting the original payload unchanged, write body bytes

docs/design/028-document-and-record-output.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,16 @@ The rule is:
136136
value without an explicit format, redirected output should default to pretty
137137
JSON; explicit `--rsh-print=b` selects compact rendered output for scripts
138138

139-
Practically, saving a negotiated CBOR response keeps CBOR bytes:
139+
Practically, saving a CBOR response keeps CBOR bytes:
140140

141141
```bash
142-
restish api.rest.sh/content/cbor > response.cbor
142+
restish api.rest.sh/formats/cbor > response.cbor
143143
```
144144

145145
Converting that same response to JSON is explicit:
146146

147147
```bash
148-
restish api.rest.sh/content/cbor -o json > response.json
148+
restish api.rest.sh/formats/cbor -o json > response.json
149149
```
150150

151151
### `-o json` Preserves Document Framing

internal/cli/generated_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1902,7 +1902,7 @@ func TestGeneratedCommandAcceptHeaderIncludesDeclaredSupportedResponseTypes(t *t
19021902
if err := c.Run([]string{"restish", "tapi", "get-negotiated"}); err != nil {
19031903
t.Fatalf("get-negotiated: %v", err)
19041904
}
1905-
want := "application/cbor;q=0.9, application/vnd.example+json;q=0.5, application/json;q=0.5, text/plain;q=0.2"
1905+
want := "application/vnd.example+json;q=0.9, application/json;q=0.9, application/cbor;q=0.6, text/plain;q=0.2"
19061906
if gotAccept != want {
19071907
t.Fatalf("Accept = %q, want %q", gotAccept, want)
19081908
}

internal/content/defaults.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func Default() *Registry {
3636
Name: "json",
3737
MIMETypes: []string{"application/json"},
3838
Suffixes: []string{"+json"},
39-
Quality: 0.5,
39+
Quality: 0.9,
4040
Marshal: func(v any) ([]byte, error) {
4141
return json.Marshal(v)
4242
},
@@ -55,7 +55,7 @@ func Default() *Registry {
5555
r.AddContentType(&ContentType{
5656
Name: "ndjson",
5757
MIMETypes: []string{"application/x-ndjson", "application/ndjson", "application/jsonl", "application/jsonlines"},
58-
Quality: 0.5,
58+
Quality: 0.8,
5959
Marshal: marshalNDJSON,
6060
Unmarshal: func(data []byte) (any, error) {
6161
lines := bytes.Split(data, []byte{'\n'})
@@ -90,7 +90,7 @@ func Default() *Registry {
9090
Name: "yaml",
9191
MIMETypes: []string{"application/yaml", "application/x-yaml", "text/yaml", "text/x-yaml"},
9292
Suffixes: []string{"+yaml"},
93-
Quality: 0.5,
93+
Quality: 0.8,
9494
Marshal: func(v any) ([]byte, error) {
9595
return yaml.Marshal(v)
9696
},
@@ -107,7 +107,7 @@ func Default() *Registry {
107107
Name: "cbor",
108108
MIMETypes: []string{"application/cbor"},
109109
Suffixes: []string{"+cbor"},
110-
Quality: 0.9,
110+
Quality: 0.6,
111111
Marshal: func(v any) ([]byte, error) {
112112
return cbor.Marshal(v)
113113
},
@@ -124,7 +124,7 @@ func Default() *Registry {
124124
Name: "msgpack",
125125
MIMETypes: []string{"application/msgpack", "application/x-msgpack", "application/vnd.msgpack"},
126126
Suffixes: []string{"+msgpack"},
127-
Quality: 0.8,
127+
Quality: 0.6,
128128
Marshal: func(v any) ([]byte, error) {
129129
return msgpack.Marshal(v)
130130
},
@@ -165,7 +165,7 @@ func Default() *Registry {
165165
Name: "ion",
166166
MIMETypes: []string{"application/ion", "text/ion"},
167167
Suffixes: []string{"+ion"},
168-
Quality: 0.8,
168+
Quality: 0.6,
169169
Marshal: func(v any) ([]byte, error) {
170170
return ion.MarshalText(v)
171171
},
@@ -178,8 +178,6 @@ func Default() *Registry {
178178
},
179179
})
180180

181-
// text/* is a catch-all for plain text responses — lowest quality so
182-
// structured formats are always preferred.
183181
r.AddContentType(&ContentType{
184182
Name: "form",
185183
MIMETypes: []string{"application/x-www-form-urlencoded"},
@@ -238,6 +236,8 @@ func Default() *Registry {
238236
},
239237
})
240238

239+
// text/* is a catch-all for plain text responses. Keep it below explicit
240+
// structured types so it does not win when a more specific format is known.
241241
r.AddContentType(&ContentType{
242242
Name: "text",
243243
MIMETypes: []string{"text/plain", "text/*"},

internal/content/registry_test.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,18 +146,32 @@ func TestMakeJSONSafeIntegerKeys(t *testing.T) {
146146

147147
func TestAcceptHeader(t *testing.T) {
148148
h := reg.AcceptHeader()
149-
// cbor q=0.9 must appear before json q=0.5
149+
// JSON is the least surprising default negotiation target. Binary
150+
// structured formats remain supported, but should not win by default.
150151
iCBOR := strings.Index(h, "application/cbor")
151152
iJSON := strings.Index(h, "application/json")
152-
if iCBOR == -1 || iJSON == -1 {
153+
iYAML := strings.Index(h, "application/yaml")
154+
if iCBOR == -1 || iJSON == -1 || iYAML == -1 {
153155
t.Fatalf("Accept header missing expected types: %q", h)
154156
}
155-
if iCBOR > iJSON {
156-
t.Errorf("cbor should appear before json in Accept header: %q", h)
157+
if iJSON > iYAML {
158+
t.Errorf("json should appear before yaml in Accept header: %q", h)
159+
}
160+
if iYAML > iCBOR {
161+
t.Errorf("yaml should appear before cbor in Accept header: %q", h)
162+
}
163+
if iJSON > iCBOR {
164+
t.Errorf("json should appear before cbor in Accept header: %q", h)
157165
}
158166
if !strings.Contains(h, "application/x-ndjson") {
159167
t.Fatalf("Accept header missing application/x-ndjson: %q", h)
160168
}
169+
if !strings.Contains(h, "application/x-ndjson;q=0.8") {
170+
t.Fatalf("Accept header should prefer NDJSON below JSON: %q", h)
171+
}
172+
if !strings.Contains(h, "application/yaml;q=0.8") {
173+
t.Fatalf("Accept header should prefer YAML below JSON: %q", h)
174+
}
161175
iSSE := strings.Index(h, "text/event-stream")
162176
if iSSE == -1 {
163177
t.Fatalf("text/event-stream missing from Accept header: %q", h)
@@ -310,7 +324,7 @@ func TestAcceptHeaderForUsesRequestedSupportedMediaTypes(t *testing.T) {
310324
"text/plain",
311325
"application/json",
312326
})
313-
want := "application/cbor;q=0.9, application/vnd.example+json;q=0.5, application/json;q=0.5, text/plain;q=0.2"
327+
want := "application/vnd.example+json;q=0.9, application/json;q=0.9, application/cbor;q=0.6, text/plain;q=0.2"
314328
if got != want {
315329
t.Fatalf("AcceptHeaderFor() = %q, want %q", got, want)
316330
}

site/content/en/docs/guides/output.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,22 +115,24 @@ CBOR, YAML, images, octet streams, zip files, text, and unknown payloads:
115115
```bash
116116
restish api.rest.sh/images/jpeg > dragonfly.jpg
117117
restish api.rest.sh/bytes/64 > sample.bin
118-
restish api.rest.sh/content/cbor > response.cbor
118+
restish api.rest.sh/formats/cbor > response.cbor
119119
```
120120

121121
Choose an output format when you want Restish to transform the decoded body:
122122

123123
```bash
124-
restish api.rest.sh/content/cbor -o json > response.json
124+
restish api.rest.sh/formats/cbor -o json > response.json
125125
```
126126

127127
Raw redirected output bypasses Restish's structured body decoding and
128128
formatting for presentation, but it is still based on the body after HTTP
129-
content-encoding decompression. `raw` is not an `-o` format. To save bytes
130-
unchanged, redirect stdout without choosing a filter, metadata shortcut,
131-
collection, or explicit output format. Response middleware plugins are skipped
132-
on this raw-download path; they run when Restish renders, filters, collects, or
133-
prints an interpreted response.
129+
content-encoding decompression. It also does not make Restish ask the server
130+
for a different representation: default `Accept` negotiation still prefers
131+
JSON and other text-friendly structured formats unless you set `Accept`
132+
yourself. `raw` is not an `-o` format. To save bytes unchanged, redirect stdout
133+
without choosing a filter, metadata shortcut, collection, or explicit output
134+
format. Response middleware plugins are skipped on this raw-download path; they
135+
run when Restish renders, filters, collects, or prints an interpreted response.
134136

135137
Control exactly what stdout contains with `--rsh-print`:
136138

site/content/en/docs/guides/troubleshooting.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,17 @@ Add `--rsh-print=b` when the script needs compact rendered JSON.
5454
**Symptom:** A redirected response is CBOR, YAML, text, or another server format
5555
instead of JSON.
5656

57-
**Likely cause:** Redirected unfiltered output saves response body bytes.
57+
**Likely cause:** Redirected unfiltered output saves response body bytes. This
58+
can happen when the endpoint only returns that format, a profile or command set
59+
an `Accept` header, or the server ignored Restish's JSON-first default
60+
negotiation.
5861

5962
**How to confirm:** Run with an explicit format.
6063

6164
**Fix:**
6265

6366
```bash
64-
restish api.rest.sh/content/cbor -o json > response.json
67+
restish api.rest.sh/formats/cbor -o json > response.json
6568
```
6669

6770
**Prevention:** Use `-o json` when a file or script needs JSON. Omit `-o` when
@@ -109,7 +112,9 @@ legacy location so they are not imported again.
109112
instead of JSON.
110113

111114
**Likely cause:** Restish sends an `Accept` header listing all registered
112-
content types with quality values.
115+
content types with quality values. The default header prefers JSON and other
116+
text-friendly structured formats, but servers can still choose another
117+
supported response type or ignore client preferences.
113118

114119
**How to confirm:** Inspect the request with `/headers` or verbose mode.
115120

site/content/en/docs/recipes/save-a-response-unchanged.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ raw-download path, so installed plugins cannot silently alter saved files.
1313

1414
```bash
1515
restish api.rest.sh/bytes/64 > sample.bin
16-
restish api.rest.sh/content/cbor > response.cbor
16+
restish api.rest.sh/formats/cbor > response.cbor
1717
```
1818

1919
For an image:
@@ -28,5 +28,8 @@ Redirected byte output still uses the body that Go's HTTP client exposes after
2828
any HTTP content-encoding decompression; it is not a packet capture of the exact
2929
wire transfer. The distinction is part of Restish's
3030
[output defaults](/docs/reference/output-defaults/).
31+
Restish's default `Accept` header still prefers JSON and other text-friendly
32+
structured formats; set `Accept` yourself when you want the server to send a
33+
binary structured format such as CBOR.
3134

3235
Related: [Output](/docs/guides/output/).

site/content/en/docs/reference/content-types.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,20 @@ restish -H 'Accept: image/png' api.rest.sh/images/png
104104
```
105105

106106
Restish generates an `Accept` header from registered content types, ordered by
107-
quality and deduplicated by canonical MIME type. If a plugin registers the same
108-
MIME type later, that later registration is the effective one.
107+
quality and deduplicated by canonical MIME type. Defaults prefer JSON and
108+
vendor JSON first, then other text-friendly structured formats such as NDJSON
109+
and YAML, before binary structured formats such as CBOR, MessagePack, and Ion.
110+
That ordering is least surprising in terminals, scripts, logs, and tools.
111+
Binary formats are still supported; request one explicitly with `-H`, a
112+
profile, or an endpoint that only returns that format when the API and your
113+
workflow benefit from it. If a plugin registers the same MIME type later, that
114+
later registration is the effective one.
109115

110116
Use `-o` when you want Restish to transform a decoded response after it arrives:
111117

112118
```bash
113-
restish api.rest.sh/content/cbor -o json > response.json
119+
restish api.rest.sh/formats/cbor > response.cbor
120+
restish api.rest.sh/formats/cbor -o json > response.json
114121
```
115122

116123
## Compression

0 commit comments

Comments
 (0)