projectdiscovery/nuclei · error

downloaded content is not valid JSON: %w

Error message

downloaded content is not valid JSON: %w

What it means

The OpenAPI spec URL returned HTTP 200 but the body is not parseable JSON. The downloader reads at most 10MB (io.LimitReader) and unmarshals into map[string]interface{}; any failure is wrapped with the underlying encoding/json error. Common causes: an HTML page (docs UI or login redirect served with 200), a YAML or plain-text body, a BOM, or a document truncated by the 10MB cap.

Source

Thrown at pkg/input/formats/openapi/downloader.go:66

	}

	defer func() {
		_ = resp.Body.Close()
	}()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("HTTP %d when downloading OpenAPI spec", resp.StatusCode)
	}

	bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, maxSpecSizeBytes))
	if err != nil {
		return "", errors.Wrap(err, "failed to read response body")
	}

	// Validate it's a valid JSON and has OpenAPI structure
	var spec map[string]interface{}
	if err := json.Unmarshal(bodyBytes, &spec); err != nil {
		return "", fmt.Errorf("downloaded content is not valid JSON: %w", err)
	}

	// Check if it's an OpenAPI 3.0 spec
	if openapi, exists := spec["openapi"]; exists {
		if openapiStr, ok := openapi.(string); ok && strings.HasPrefix(openapiStr, "3.") {
			// Valid OpenAPI 3.0 spec
		} else {
			return "", fmt.Errorf("not a valid OpenAPI 3.0 spec (found version: %v)", openapi)
		}
	} else {
		return "", fmt.Errorf("not an OpenAPI spec (missing 'openapi' field)")
	}

	// Extract host from URL for server configuration
	parsedURL, err := url.Parse(urlStr)
	if err != nil {
		return "", errors.Wrap(err, "failed to parse URL")
	}

View on GitHub (pinned to 265b3a3dec)

Solutions

  1. Fetch the URL with curl and inspect the raw body to see what is actually served
  2. If the body is YAML, switch to `-im swagger` (accepts .yaml/.yml) or convert the file to JSON and use `nuclei -l spec.json`
  3. Ensure the URL returns the raw spec document, not an HTML viewer or login page
  4. If the spec exceeds 10MB, split it or trim unused paths, then feed it as a local file

Example fix

# before: .json URL that actually serves YAML
nuclei -im openapi -u https://host/spec.json

# after: download locally and convert
curl -s https://host/spec.json -o spec.yaml
# convert YAML->JSON (e.g. yq -o=json '.' spec.yaml > spec.json)
nuclei -l spec.json
Defensive patterns

Strategy: validation

Validate before calling

resp, err := http.Get(url)
if err != nil {
    return err
}
body, _ := io.ReadAll(io.LimitReader(resp.Body, 10<<20))
if !json.Valid(body) {
    return fmt.Errorf("body at %s is not JSON; inspect it before passing to nuclei", url)
}

Type guard

func looksLikeOpenAPIJSON(body []byte) bool {
    var spec map[string]json.RawMessage
    if err := json.Unmarshal(body, &spec); err != nil {
        return false
    }
    _, hasOpenAPI := spec["openapi"]
    _, hasSwagger := spec["swagger"]
    return hasOpenAPI || hasSwagger
}

Try / catch

if err := downloader.Download(...); err != nil {
    if strings.Contains(err.Error(), "not valid JSON") {
        // body is HTML/YAML: fetch manually, convert, then use a local file
    }
}

Prevention

When it happens

Trigger: URL ends in .json but content negotiation returns HTML; SSO/login portals answering 200 with an HTML shell; a YAML body served under a .json name; specs larger than 10MB cut mid-document by the LimitReader; gzip/deflate double-encoding producing binary bytes.

Common situations: Docs sites where /openapi.json actually renders a viewer page; reverse proxies that inject scripts or cookies notices into responses; misconfigured static hosting guessing content type; very large machine-generated specs.

Related errors


AI-assisted analysis of projectdiscovery/nuclei@265b3a3dec (2026-08-15). Data as JSON: /api/errors/289bb5259fa332ba. Report an issue: GitHub.