package apiutil import ( "mime" "sort" "strconv" "strings" ) type mediaRange struct { mt string // canonicalised media‑type, e.g. "application/json" q float64 // quality factor (0‑1) raw string // original string – useful for logging/debugging spec int // 2=exact, 1=type/*, 0=*/* } func parseMediaRange(field string) (mediaRange, bool) { field = strings.TrimSpace(field) mt, params, err := mime.ParseMediaType(field) if err != nil { log.WithError(err).Debug("Failed to parse header field") return mediaRange{}, false } r := mediaRange{mt: mt, q: 1, spec: 2, raw: field} if qs, ok := params["q"]; ok { v, err := strconv.ParseFloat(qs, 64) if err != nil || v < 0 || v > 1 { log.WithField("q", qs).Debug("Invalid quality factor (0‑1)") return mediaRange{}, false // skip invalid entry } r.q = v } switch { case mt == "*/*": r.spec = 0 case strings.HasSuffix(mt, "/*"): r.spec = 1 } return r, true } func hasExplicitQ(r mediaRange) bool { return strings.Contains(strings.ToLower(r.raw), ";q=") } // ParseAccept returns media ranges sorted by q (desc) then specificity. func ParseAccept(header string) []mediaRange { if header == "" { return []mediaRange{{mt: "*/*", q: 1, spec: 0, raw: "*/*"}} } var out []mediaRange for field := range strings.SplitSeq(header, ",") { if r, ok := parseMediaRange(field); ok { out = append(out, r) } } sort.SliceStable(out, func(i, j int) bool { ei, ej := hasExplicitQ(out[i]), hasExplicitQ(out[j]) if ei != ej { return ei // explicit beats implicit } if out[i].q != out[j].q { return out[i].q > out[j].q } return out[i].spec > out[j].spec }) return out } // Matches reports whether content type is acceptable per the header. func Matches(header, ct string) bool { for _, r := range ParseAccept(header) { switch { case r.q == 0: continue case r.mt == "*/*": return true case strings.HasSuffix(r.mt, "/*"): if strings.HasPrefix(ct, r.mt[:len(r.mt)-1]) { return true } case r.mt == ct: return true } } return false } // Negotiate selects the best server type according to the header. // Returns the chosen type and true, or "", false when nothing matches. func Negotiate(header string, serverTypes []string) (string, bool) { for _, r := range ParseAccept(header) { if r.q == 0 { continue } for _, s := range serverTypes { if Matches(r.mt, s) { return s, true } } } return "", false } // PrimaryAcceptMatches only checks if the first accept matches func PrimaryAcceptMatches(header, produced string) bool { for _, r := range ParseAccept(header) { if r.q == 0 { continue // explicitly unacceptable – skip } return Matches(r.mt, produced) } return false }