feat(api): add --verbose to show the request and response headers

--verbose prints the method, URL without query and headers of each request, then the status and headers of its response, on stderr like curl -v. Credentials in Authorization, Proxy-Authorization and Cookie are hidden.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 23:50:46 +02:00
1 parent 83dd4f7938
commit 6c124fc998
2 files changed
+53 -9

No files matched your search

+36 -9
View File
@@ -149,7 +149,7 @@ $ tea api -X PATCH repos/alice/project --input settings.json`
func (a *application) apiCommand() *cobra.Command {
var method, input, output string
var raw, typed, headerValues []string
var include, paginate, silent bool
var include, verbose, paginate, silent bool
var maxPages int
cmd := &cobra.Command{Use: "api <endpoint>", Short: "Make an authenticated Gitea API request", Long: apiLong, Example: apiExamples, Annotations: map[string]string{"tea.text": "api returns raw responses", "tea.rawjson": "true"}, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
usage := func(err error) error { return clierr.Wrap(clierr.Usage, err) }
@@ -257,8 +257,13 @@ func (a *application) apiCommand() *cobra.Command {
response.color = response.tty && a.colorEnabled()
}
return (services.API{Client: target.client}).Visit(cmd.Context(), services.APIOptions{Endpoint: endpoint, Method: method, Body: body, Headers: headers, Paginate: paginate, MaxPages: maxPages}, func(r *http.Response) error {
if include {
if err := writeResponseHead(cmd.ErrOrStderr(), r); err != nil {
if verbose {
if err := writeRequestHead(cmd.ErrOrStderr(), r.Request); err != nil {
return err
}
}
if include || verbose {
if err := writeResponseHead(cmd.ErrOrStderr(), r, verbose); err != nil {
return err
}
}
@@ -277,6 +282,7 @@ func (a *application) apiCommand() *cobra.Command {
cmd.Flags().StringArrayVarP(&typed, "field", "F", nil, "Add a typed field: `name=value`; @FILE or @- reads a file or stdin")
cmd.Flags().StringVar(&output, "output-file", "", "Write the complete response to a new `file`")
cmd.Flags().BoolVarP(&include, "include", "i", false, "Print the response status and headers to stderr")
cmd.Flags().BoolVar(&verbose, "verbose", false, "Print the request and the response status and headers to stderr")
cmd.Flags().BoolVar(&silent, "silent", false, "Do not print the response body")
cmd.Flags().BoolVar(&paginate, "paginate", false, "Follow next-page links and print every page")
cmd.Flags().IntVar(&maxPages, "max-pages", 1000, "Stop --paginate after this many pages")
@@ -337,13 +343,34 @@ func (l *lastByte) Write(p []byte) (int, error) {
return l.w.Write(p)
}
// writeResponseHead prints the status line and the sorted headers of r, then a blank line.
func writeResponseHead(w io.Writer, r *http.Response) error {
// writeResponseHead prints the status line and the sorted headers of r, then a blank line; verbose
// marks each line with "< " like curl.
func writeResponseHead(w io.Writer, r *http.Response, verbose bool) error {
prefix := ""
if verbose {
prefix = "< "
}
return writeHead(w, prefix, r.Proto+" "+r.Status, r.Header)
}
// writeRequestHead prints the method, URL without query and headers of r marked with "> ", hiding
// credentials.
func writeRequestHead(w io.Writer, r *http.Request) error {
headers := r.Header.Clone()
for _, name := range []string{"Authorization", "Proxy-Authorization", "Cookie"} {
if headers.Get(name) != "" {
headers.Set(name, "(hidden)")
}
}
return writeHead(w, "> ", r.Method+" "+r.URL.Scheme+"://"+r.URL.Host+r.URL.EscapedPath(), headers)
}
func writeHead(w io.Writer, prefix, first string, headers http.Header) error {
var b strings.Builder
fmt.Fprintf(&b, "%s %s\n", r.Proto, r.Status)
for _, key := range slices.Sorted(maps.Keys(r.Header)) {
for _, value := range r.Header.Values(key) {
fmt.Fprintf(&b, "%s: %s\n", key, value)
b.WriteString(prefix + first + "\n")
for _, key := range slices.Sorted(maps.Keys(headers)) {
for _, value := range headers.Values(key) {
b.WriteString(prefix + terminal.SafeLine(key+": "+value) + "\n")
}
}
b.WriteString("\n")
+17
View File
@@ -265,6 +265,23 @@ func TestAPIFiltersAndFormatsJSONResponses(t *testing.T) {
requireUsageBeforeRequests(t, "api returns raw responses; --json is not supported", "api", "version", "--json")
}
func TestAPIVerboseShowsRequestAndResponseWithoutCredentials(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-Tea-Test", "response")
_, _ = w.Write([]byte(`{}`))
}))
defer server.Close()
deps, out, diagnostics := serverDeps(t, server.URL)
require.NoError(t, Execute(context.Background(), deps, []string{"api", "version?access_token=secret", "--verbose", "-H", "X-Request: one"}))
require.Equal(t, "{}", out.String())
head := diagnostics.String()
for _, want := range []string{"> GET " + server.URL + "/api/v1/version\n", "> Authorization: (hidden)\n", "> X-Request: one\n", "< HTTP/1.1 200 OK\n", "< X-Tea-Test: response\n"} {
require.Contains(t, head, want)
}
require.NotContains(t, head, "secret")
require.NotContains(t, head, "token token")
}
func TestAPIHelpShowsJQ(t *testing.T) {
deps, out, _ := testDeps(t)
require.NoError(t, Execute(context.Background(), deps, []string{"api", "--help"}))