Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 0 additions & 62 deletions internal/fetch/charset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package fetch

import (
"io"
"net/http"
"strings"
"testing"
)
Expand Down Expand Up @@ -124,64 +123,3 @@ func TestTranscodeReader(t *testing.T) {
})
}
}

func TestGetContentTypeCharset(t *testing.T) {
tests := []struct {
name string
contentType string
wantType ContentType
wantCharset string
}{
{
name: "json with charset",
contentType: "application/json; charset=utf-8",
wantType: TypeJSON,
wantCharset: "utf-8",
},
{
name: "html with charset",
contentType: "text/html; charset=iso-8859-1",
wantType: TypeHTML,
wantCharset: "iso-8859-1",
},
{
name: "json without charset",
contentType: "application/json",
wantType: TypeJSON,
wantCharset: "",
},
{
name: "empty content type",
contentType: "",
wantType: TypeUnknown,
wantCharset: "",
},
{
name: "xml with charset",
contentType: "text/xml; charset=windows-1252",
wantType: TypeXML,
wantCharset: "windows-1252",
},
{
name: "csv with charset",
contentType: "text/csv; charset=shift_jis",
wantType: TypeCSV,
wantCharset: "shift_jis",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
headers := http.Header{}
if tt.contentType != "" {
headers.Set("Content-Type", tt.contentType)
}
gotType, gotCharset := getContentType(headers)
if gotType != tt.wantType {
t.Errorf("getContentType() type = %v, want %v", gotType, tt.wantType)
}
if gotCharset != tt.wantCharset {
t.Errorf("getContentType() charset = %q, want %q", gotCharset, tt.wantCharset)
}
})
}
}
164 changes: 26 additions & 138 deletions internal/fetch/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"os"
Expand All @@ -33,24 +32,6 @@ import (
// formatting a response body or copying it to the clipboard.
const maxBodyBytes = 1 << 20 // 1MiB

type ContentType int

const (
TypeUnknown ContentType = iota
TypeCSS
TypeCSV
TypeGRPC
TypeHTML
TypeImage
TypeJSON
TypeMsgPack
TypeNDJSON
TypeProtobuf
TypeSSE
TypeXML
TypeYAML
)

type Request struct {
AWSSigv4 *aws.Config
Basic *core.KeyVal[string]
Expand Down Expand Up @@ -390,21 +371,20 @@ func formatResponse(ctx context.Context, r *Request, resp *http.Response) (io.Re
}

p := r.PrinterHandle.Stdout()
contentType, charset := getContentType(resp.Header)
switch contentType {
case TypeGRPC:
// NOTE: This bypasses the isPrintable check for binary data.
contentType, charset := format.GetContentType(resp.Header)

// gRPC streaming needs the response descriptor — handle inline.
if contentType == format.TypeGRPC {
return nil, format.FormatGRPCStream(resp.Body, r.responseDescriptor, p)
case TypeNDJSON:
// NOTE: This bypasses the isPrintable check for binary data.
return nil, format.FormatNDJSON(transcodeReader(resp.Body, charset), p)
case TypeSSE:
// NOTE: This bypasses the isPrintable check for binary data.
return nil, format.FormatEventStream(transcodeReader(resp.Body, charset), p)
}

// Dispatch registered streaming formatters (NDJSON, SSE).
if fn := format.GetStreaming(contentType); fn != nil {
return nil, fn(transcodeReader(resp.Body, charset), p)
}

// If image rendering is disabled, return the reader immediately.
if contentType == TypeImage && r.Image == core.ImageOff {
if contentType == format.TypeImage && r.Image == core.ImageOff {
return resp.Body, nil
}

Expand All @@ -418,134 +398,42 @@ func formatResponse(ctx context.Context, r *Request, resp *http.Response) (io.Re
}

// If the Content-Type is unknown, attempt to sniff the body.
if contentType == TypeUnknown {
contentType = sniffContentType(buf)
if contentType == TypeUnknown {
if contentType == format.TypeUnknown {
contentType = format.SniffContentType(buf)
if contentType == format.TypeUnknown {
return bytes.NewReader(buf), nil
}
if contentType == TypeImage && r.Image == core.ImageOff {
if contentType == format.TypeImage && r.Image == core.ImageOff {
return bytes.NewReader(buf), nil
}
}

// Transcode non-UTF-8 text to UTF-8, skipping binary formats.
switch contentType {
case TypeImage, TypeMsgPack, TypeProtobuf:
case format.TypeImage, format.TypeMsgPack, format.TypeProtobuf:
default:
buf = transcodeBytes(buf, charset)
}

switch contentType {
case TypeCSS:
if format.FormatCSS(buf, p) == nil {
buf = p.Bytes()
}
case TypeCSV:
if format.FormatCSV(buf, p) == nil {
buf = p.Bytes()
}
case TypeHTML:
if format.FormatHTML(buf, p) == nil {
buf = p.Bytes()
}
case TypeImage:
// Special cases that need extra context beyond ([]byte, *Printer).
if contentType == format.TypeImage {
return nil, image.Render(ctx, buf, r.Image == core.ImageNative)
case TypeJSON:
if format.FormatJSON(buf, p) == nil {
buf = p.Bytes()
}
case TypeMsgPack:
if format.FormatMsgPack(buf, p) == nil {
buf = p.Bytes()
}
case TypeProtobuf:
var err error
if r.responseDescriptor != nil {
err = format.FormatProtobufWithDescriptor(buf, r.responseDescriptor, p)
} else {
err = format.FormatProtobuf(buf, p)
}
if err == nil {
buf = p.Bytes()
}
case TypeXML:
if format.FormatXML(buf, p) == nil {
buf = p.Bytes()
}
case TypeYAML:
if format.FormatYAML(buf, p) == nil {
}
if contentType == format.TypeProtobuf && r.responseDescriptor != nil {
if format.FormatProtobufWithDescriptor(buf, r.responseDescriptor, p) == nil {
buf = p.Bytes()
}
return bytes.NewReader(buf), nil
}

return bytes.NewReader(buf), nil
}

func getContentType(headers http.Header) (ContentType, string) {
contentType := headers.Get("Content-Type")
if contentType == "" {
return TypeUnknown, ""
}
mediaType, params, err := mime.ParseMediaType(contentType)
if err != nil {
return TypeUnknown, ""
}
charset := params["charset"]

if typ, subtype, ok := strings.Cut(mediaType, "/"); ok {
switch typ {
case "image":
return TypeImage, charset
case "application":
switch subtype {
case "csv":
return TypeCSV, charset
case "grpc", "grpc+proto":
return TypeGRPC, charset
case "json":
return TypeJSON, charset
case "msgpack", "x-msgpack", "vnd.msgpack":
return TypeMsgPack, charset
case "x-ndjson", "ndjson", "x-jsonl", "jsonl", "x-jsonlines":
return TypeNDJSON, charset
case "protobuf", "x-protobuf", "x-google-protobuf", "vnd.google.protobuf":
return TypeProtobuf, charset
case "xml":
return TypeXML, charset
case "yaml", "x-yaml":
return TypeYAML, charset
}
if strings.HasSuffix(subtype, "+json") || strings.HasSuffix(subtype, "-json") {
return TypeJSON, charset
}
if strings.HasSuffix(subtype, "+proto") {
return TypeProtobuf, charset
}
if strings.HasSuffix(subtype, "+xml") {
return TypeXML, charset
}
if strings.HasSuffix(subtype, "+yaml") {
return TypeYAML, charset
}
case "text":
switch subtype {
case "css":
return TypeCSS, charset
case "csv":
return TypeCSV, charset
case "html":
return TypeHTML, charset
case "event-stream":
return TypeSSE, charset
case "xml":
return TypeXML, charset
case "yaml", "x-yaml":
return TypeYAML, charset
}
// Dispatch registered buffered formatters.
if fn := format.GetBuffered(contentType); fn != nil {
if fn(buf, p) == nil {
buf = p.Bytes()
}
}

return TypeUnknown, charset
return bytes.NewReader(buf), nil
}

func streamToStdout(r io.Reader, p *core.Printer, forceOutput, noPager bool) error {
Expand Down
Loading