JuliusBrussee/caveman · error

repeated path separators are not allowed

Error message

repeated path separators are not allowed

What it means

validatePathComponents rejects empty path segments in the middle of a path (i.e. '//'), while allowing the leading and trailing empty segments of '/compat/x/' style paths. Repeated separators can bypass prefix matching or be collapsed differently by intermediate proxies, changing route identity.

Solutions

  1. Remove the empty segment(s): collapse '//' to '/' in the request path
  2. Fix base/path string concatenation to avoid a double slash
  3. Clean the configured BaseURL so its path has no '//'
  4. Use url.JoinPath or the adapter's joinCompatPath instead of manual concatenation

Example fix

// before
GET /compat/openai//v1/chat/completions

// after
GET /compat/openai/v1/chat/completions
Defensive patterns

Strategy: validation

Validate before calling

u, err := url.Parse(rawURL)
if err == nil && strings.Contains(strings.TrimPrefix(u.Path, "/"), "//") {
    return errors.New("double slash in path")
}

Try / catch

if err := ValidateBaseURL(base); err != nil {
    return fmt.Errorf("bad upstream base url: %w", err)
}

Prevention

When it happens

Trigger: Requests like /compat/openai//v1/chat/completions; base URL config strings with double slashes in the path portion; clients concatenating base and suffix paths carelessly producing 'a//b'.

Common situations: String-joining a base URL ending in '/' with a path starting with '/'; proxies that normalized then re-emitted double slashes; config typos.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/dffc6dc82918195d. Report an issue: GitHub.

Appendix: source

Thrown at proxy/providers/openaicompat/openaicompat.go:494

func validateCompatPath(path, rawPath string) error {
	if !strings.HasPrefix(path, "/compat/") {
		return nil
	}
	if err := validatePathComponents(path, rawPath); err != nil {
		return fmt.Errorf("compat route path rejected: %w", err)
	}
	return nil
}

func validatePathComponents(path, rawPath string) error {
	if strings.Contains(path, `\`) {
		return fmt.Errorf("backslash is not allowed in path")
	}
	segments := strings.Split(path, "/")
	for i, segment := range segments {
		if segment == "" && i > 0 && i < len(segments)-1 {
			return fmt.Errorf("repeated path separators are not allowed")
		}
		if segment == "." || segment == ".." {
			return fmt.Errorf("dot segments are not allowed in path")
		}
	}
	// URL.Path is decoded by net/url while RawPath retains a valid escaped
	// spelling. Reject separators, backslashes, and dot bytes in either form so
	// a path cannot change route identity after another decoder or proxy hop.
	for _, escape := range []string{"%2f", "%5c", "%2e"} {
		if strings.Contains(strings.ToLower(path), escape) || strings.Contains(strings.ToLower(rawPath), escape) {
			return fmt.Errorf("ambiguous escaped path sequence %s", escape)
		}
	}
	return nil
}

func parseBaseURL(raw, provider string) (*url.URL, error) {
	if strings.TrimSpace(raw) == "" {

View on GitHub (pinned to 3ee70a1026)