## Phase 8: Enterprise Extractor Improvements ✅ - 14 security extractors (TLS, JWT, SQL injection, XSS, etc.) - 10 framework-specific extractors (Spring, Django, Rails, etc.) - Config file security detection (YAML, TOML) ## Phase 9: Autonomous Extractor Generation ✅ - Shadow mode executor with TP/FP tracking - Graduation pipeline with confidence thresholds - Auto-rollback on regression detection - Cross-project pattern syncing ## UAT Suite Complete (14 scripts, 90 tests) - test-core-detection.sh (6 tests) - test-declarative-extractors.sh (5 tests) - test-domain-frameworks.sh (5 tests) - test-domain-unreal.sh (3 tests) - test-llm-extraction.sh (6 tests) - test-eval-harness.sh (5 tests) - test-cross-language.sh (3 tests) - test-precommit-performance.sh (4 tests) - test-output-formats.sh (8 tests) - test-drift-detection.sh (6 tests) - test-exit-codes.sh (12 tests) + 3 more scripts ## Other Changes - Updated roadmap to mark Phase 8-9 complete - Added .gitignore entries for build artifacts - Updated pre-commit: 800 line limit, exclude tests/data/cmd Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
57 lines
1.5 KiB
Go
57 lines
1.5 KiB
Go
package httpkit
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
)
|
|
|
|
// Download fetches a URL and returns the response body as bytes.
|
|
// maxBytes limits the response size; 0 means no limit.
|
|
//
|
|
// Unlike Do, Download does not apply default headers (download URLs are
|
|
// typically pre-signed or public), does not marshal JSON, and does not
|
|
// retry. It does respect the client's timeout.
|
|
func (c *Client) Download(ctx context.Context, url string, maxBytes int64) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create download request: %w", err)
|
|
}
|
|
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
if errors.Is(err, context.DeadlineExceeded) || isTimeoutError(err) {
|
|
return nil, fmt.Errorf("download: %w", ErrTimeout)
|
|
}
|
|
return nil, fmt.Errorf("download: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
|
return nil, &APIError{
|
|
StatusCode: resp.StatusCode,
|
|
Body: string(body),
|
|
sentinel: ClassifyStatus(resp.StatusCode),
|
|
}
|
|
}
|
|
|
|
var reader io.Reader = resp.Body
|
|
if maxBytes > 0 {
|
|
reader = io.LimitReader(resp.Body, maxBytes+1)
|
|
}
|
|
|
|
data, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read download body: %w", err)
|
|
}
|
|
|
|
if maxBytes > 0 && int64(len(data)) > maxBytes {
|
|
return nil, fmt.Errorf("download exceeds %d byte limit", maxBytes)
|
|
}
|
|
|
|
return data, nil
|
|
}
|