(ctx context.Context, url string, out *upstreamEnvelope[T])
| 131 | } |
| 132 | |
| 133 | func fetchJSON[T any](ctx context.Context, url string, out *upstreamEnvelope[T]) error { |
| 134 | var lastErr error |
| 135 | attempts := common.GetEnvOrDefault("SYNC_HTTP_RETRY", 3) |
| 136 | if attempts < 1 { |
| 137 | attempts = 1 |
| 138 | } |
| 139 | baseDelay := 200 * time.Millisecond |
| 140 | maxMB := common.GetEnvOrDefault("SYNC_HTTP_MAX_MB", 10) |
| 141 | maxBytes := int64(maxMB) << 20 |
| 142 | for attempt := 0; attempt < attempts; attempt++ { |
| 143 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) |
| 144 | if err != nil { |
| 145 | return err |
| 146 | } |
| 147 | // ETag conditional request |
| 148 | cacheMutex.RLock() |
| 149 | if et := etagCache[url]; et != "" { |
| 150 | req.Header.Set("If-None-Match", et) |
| 151 | } |
| 152 | cacheMutex.RUnlock() |
| 153 | |
| 154 | resp, err := getHTTPClient().Do(req) |
| 155 | if err != nil { |
| 156 | lastErr = err |
| 157 | // backoff with jitter |
| 158 | sleep := baseDelay * time.Duration(1<<attempt) |
| 159 | jitter := time.Duration(rand.Intn(150)) * time.Millisecond |
| 160 | time.Sleep(sleep + jitter) |
| 161 | continue |
| 162 | } |
| 163 | func() { |
| 164 | defer resp.Body.Close() |
| 165 | switch resp.StatusCode { |
| 166 | case http.StatusOK: |
| 167 | // read body into buffer for caching and flexible decode |
| 168 | limited := io.LimitReader(resp.Body, maxBytes) |
| 169 | buf, err := io.ReadAll(limited) |
| 170 | if err != nil { |
| 171 | lastErr = err |
| 172 | return |
| 173 | } |
| 174 | // cache body and ETag |
| 175 | cacheMutex.Lock() |
| 176 | if et := resp.Header.Get("ETag"); et != "" { |
| 177 | etagCache[url] = et |
| 178 | } |
| 179 | bodyCache[url] = buf |
| 180 | cacheMutex.Unlock() |
| 181 | |
| 182 | // Try decode as envelope first |
| 183 | if err := json.Unmarshal(buf, out); err != nil { |
| 184 | // Try decode as pure array |
| 185 | var arr []T |
| 186 | if err2 := json.Unmarshal(buf, &arr); err2 != nil { |
| 187 | lastErr = err |
| 188 | return |
| 189 | } |
| 190 | out.Success = true |
no test coverage detected