[object Object]

← back to Cli Printing Press

fix(cli): refresh stale catalog entries and reject non-spec bodies (#286)

ee6bd881eca4e1eb1563968e54cd082bd725fecb · 2026-04-25 12:17:43 -0700 · Trevin Chow

Files touched

Diff

commit ee6bd881eca4e1eb1563968e54cd082bd725fecb
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sat Apr 25 12:17:43 2026 -0700

    fix(cli): refresh stale catalog entries and reject non-spec bodies (#286)
---
 catalog/asana.yaml                      |  2 +-
 catalog/sendgrid.yaml                   | 11 ------
 catalog/square.yaml                     | 11 ------
 internal/cli/root.go                    | 15 ++++++--
 internal/cli/spec_content_check.go      | 45 ++++++++++++++++++++++
 internal/cli/spec_content_check_test.go | 68 +++++++++++++++++++++++++++++++++
 6 files changed, 126 insertions(+), 26 deletions(-)

diff --git a/catalog/asana.yaml b/catalog/asana.yaml
index 119ae888..ec373e0e 100644
--- a/catalog/asana.yaml
+++ b/catalog/asana.yaml
@@ -2,7 +2,7 @@ name: asana
 display_name: Asana
 description: Work management and project tracking API
 category: project-management
-spec_url: https://raw.githubusercontent.com/Asana/openapi/main/defs/asana_oas.yaml
+spec_url: https://raw.githubusercontent.com/Asana/openapi/master/defs/asana_oas.yaml
 spec_format: yaml
 openapi_version: "3.0"
 tier: official
diff --git a/catalog/sendgrid.yaml b/catalog/sendgrid.yaml
deleted file mode 100644
index e215b5a6..00000000
--- a/catalog/sendgrid.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-name: sendgrid
-display_name: SendGrid
-description: Email delivery and marketing API
-category: marketing
-spec_url: https://raw.githubusercontent.com/twilio/sendgrid-oai/main/spec/json/sendgrid_oai.json
-spec_format: json
-openapi_version: "3.0"
-tier: official
-verified_date: "2026-03-23"
-homepage: https://docs.sendgrid.com/api-reference
-notes: None.
diff --git a/catalog/square.yaml b/catalog/square.yaml
deleted file mode 100644
index a09fdb5b..00000000
--- a/catalog/square.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-name: square
-display_name: Square
-description: Payment processing and commerce API
-category: payments
-spec_url: https://raw.githubusercontent.com/square/square-openapi/master/openapi.json
-spec_format: json
-openapi_version: "3.0"
-tier: official
-verified_date: "2026-03-23"
-homepage: https://developer.squareup.com/reference/square
-notes: None.
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 4a942e7b..319cb88f 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -641,11 +641,20 @@ func inferTrafficAnalysisPath(specFiles []string, specSource string) string {
 }
 
 func readSpec(specFile string, refresh bool, skipCache bool) ([]byte, error) {
+	var data []byte
+	var err error
 	if strings.HasPrefix(specFile, "http://") || strings.HasPrefix(specFile, "https://") {
-		return fetchOrCacheSpec(specFile, refresh, skipCache)
+		data, err = fetchOrCacheSpec(specFile, refresh, skipCache)
+	} else {
+		data, err = os.ReadFile(specFile)
 	}
-
-	return os.ReadFile(specFile)
+	if err != nil {
+		return nil, err
+	}
+	if rejectErr := rejectIfNotSpec(data); rejectErr != nil {
+		return nil, rejectErr
+	}
+	return data, nil
 }
 
 func mergeSpecs(specs []*spec.APISpec, name string) *spec.APISpec {
diff --git a/internal/cli/spec_content_check.go b/internal/cli/spec_content_check.go
new file mode 100644
index 00000000..0d103a4a
--- /dev/null
+++ b/internal/cli/spec_content_check.go
@@ -0,0 +1,45 @@
+package cli
+
+import (
+	"bytes"
+	"fmt"
+	"regexp"
+)
+
+// httpErrorBodyPattern matches the raw-content 404 / 5xx body GitHub serves
+// when a path is missing ("404: Not Found", "500: Internal Server Error").
+// Catches stale spec_urls whose download silently writes the error body to
+// disk and feeds it to the parser, where it surfaces several layers down as
+// a misleading "validation: name is required" or similar error.
+var httpErrorBodyPattern = regexp.MustCompile(`^[0-9]{3}: `)
+
+// rejectIfNotSpec returns a descriptive error when data plainly looks like an
+// HTTP error response or an HTML page rather than an OpenAPI / GraphQL / spec
+// document. Heuristic by design: only catches signatures that no legitimate
+// spec would produce. A real spec failing the parser is allowed through to
+// produce the parser's own error message; this gate fires only when the
+// content was never a spec in the first place.
+func rejectIfNotSpec(data []byte) error {
+	trimmed := bytes.TrimSpace(data)
+	if httpErrorBodyPattern.Match(trimmed) {
+		return fmt.Errorf("content looks like an HTTP error response, not a spec: %q", firstLineSnippet(trimmed))
+	}
+	lower := bytes.ToLower(trimmed)
+	if bytes.HasPrefix(lower, []byte("<html")) || bytes.HasPrefix(lower, []byte("<!doctype html")) {
+		return fmt.Errorf("content looks like an HTML page, not a spec")
+	}
+	return nil
+}
+
+// firstLineSnippet returns the first line of data, capped at 80 bytes, for
+// inclusion in error messages. Avoids dumping a full HTML page back at the
+// user when surfacing the rejection.
+func firstLineSnippet(data []byte) string {
+	if i := bytes.IndexByte(data, '\n'); i >= 0 {
+		data = data[:i]
+	}
+	if len(data) > 80 {
+		data = data[:80]
+	}
+	return string(data)
+}
diff --git a/internal/cli/spec_content_check_test.go b/internal/cli/spec_content_check_test.go
new file mode 100644
index 00000000..3ac03564
--- /dev/null
+++ b/internal/cli/spec_content_check_test.go
@@ -0,0 +1,68 @@
+package cli
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// TestRejectIfNotSpec covers issue #275 F-5's content-validity heuristic.
+// A stale catalog spec_url returns "404: Not Found" or an HTML error page;
+// the existing fetchOrCacheSpec status check catches HTTP-level errors, but
+// any path that hands a downloaded body to readSpec without that gate (a
+// user feeding a curl-saved 404 dump as --spec, a stale cache entry, an
+// upstream proxy that masks status codes) bypasses it. rejectIfNotSpec
+// surfaces a clear message at the boundary instead of letting the parser
+// produce "validation: name is required" two layers down.
+func TestRejectIfNotSpec(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name         string
+		data         []byte
+		wantErr      bool
+		errSubstring string
+	}{
+		{"github raw 404 body", []byte("404: Not Found"), true, "HTTP error response"},
+		{"github raw 5xx body", []byte("500: Internal Server Error\n"), true, "HTTP error response"},
+		{"html error page", []byte("<html><body>Error</body></html>"), true, "HTML page"},
+		{"html5 doctype", []byte("<!DOCTYPE html>\n<html>..."), true, "HTML page"},
+		{"openapi yaml", []byte("openapi: 3.0.0\ninfo:\n  title: x\n"), false, ""},
+		{"openapi json", []byte(`{"openapi":"3.0.0","info":{"title":"x"}}`), false, ""},
+		{"graphql sdl", []byte("type Query { hello: String }\n"), false, ""},
+		{"plain string with digits in middle", []byte("hello 404: world"), false, ""},
+		{"empty bytes", []byte(""), false, ""},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			err := rejectIfNotSpec(tc.data)
+			if tc.wantErr {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tc.errSubstring)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+// TestReadSpecRejectsLocal404Dump exercises the readSpec entry point with a
+// file containing a GitHub-raw 404 body. Captures the actual bug the F-5
+// reporter hit: their smoke methodology curled spec URLs and saved the
+// response body before invoking printing-press; stale URLs produced 14-byte
+// "404: Not Found" files that previously fed straight into the parser.
+func TestReadSpecRejectsLocal404Dump(t *testing.T) {
+	t.Parallel()
+
+	dir := t.TempDir()
+	stale := filepath.Join(dir, "stale.spec")
+	require.NoError(t, os.WriteFile(stale, []byte("404: Not Found"), 0o644))
+
+	_, err := readSpec(stale, false, true)
+	require.Error(t, err, "readSpec must reject a 404-shaped body")
+	assert.Contains(t, err.Error(), "HTTP error response")
+}

← 6361826d fix(cli): normalize object-shaped description fields before  ·  back to Cli Printing Press  ·  fix(cli): extend flag-identifier dedup to request body field 15529383 →