{"record":{"id":"0b6327b037e2b9be","repo":"tailscale/tailscale","slug":"fetching-s-s","errorCode":null,"errorMessage":"fetching %s: %s","messagePattern":"fetching (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"prober/derp.go","lineNumber":592,"sourceCode":"\t\treq, err := http.NewRequestWithContext(ctx, \"GET\", d.derpMapURL, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tres, err := httpOrFileClient.Do(req)\n\t\tif err != nil {\n\t\t\td.Lock()\n\t\t\tdefer d.Unlock()\n\t\t\tif d.lastDERPMap != nil && time.Since(d.lastDERPMapAt) < 10*time.Minute {\n\t\t\t\tlog.Printf(\"Error while fetching DERP map, using cached one: %s\", err)\n\t\t\t\t// Assume that control is restarting and use\n\t\t\t\t// the same one for a bit.\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tdefer res.Body.Close()\n\t\tif res.StatusCode != 200 {\n\t\t\treturn fmt.Errorf(\"fetching %s: %s\", d.derpMapURL, res.Status)\n\t\t}\n\t\tdm = new(tailcfg.DERPMap)\n\t\tif err := json.NewDecoder(res.Body).Decode(dm); err != nil {\n\t\t\treturn fmt.Errorf(\"decoding %s JSON: %v\", d.derpMapURL, err)\n\t\t}\n\t}\n\n\td.Lock()\n\tdefer d.Unlock()\n\td.lastDERPMap = dm\n\td.lastDERPMapAt = time.Now()\n\td.nodes = make(map[string]*tailcfg.DERPNode)\n\tfor _, reg := range d.lastDERPMap.Regions {\n\t\tif d.skipRegion(reg) {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, n := range reg.Nodes {","sourceCodeStart":574,"sourceCodeEnd":610,"githubUrl":"https://github.com/tailscale/tailscale/blob/5201273aec737d6372ab7423c31c04ca3ca2a0c2/prober/derp.go#L574-L610","documentation":"updateMap received a non-200 HTTP status when fetching the DERP map from d.derpMapURL (for example https://login.tailscale.com/derpmap/default or a self-hosted mirror); res.Status is appended. Note the cached-map fallback in updateMap only applies to transport errors - a non-200 status aborts immediately.","triggerScenarios":"The map URL returns 403/404/429/5xx: wrong or moved URL, auth-gated mirror, rate limiting, or a control/CDN incident.","commonSituations":"Typo in the -derp-map flag; corporate proxy intercepting the request; login.tailscale.com outage; a self-hosted derpmap endpoint behind auth or moved paths.","solutions":["curl -i the derpMapURL and read the exact status code and body","Fix the -derp-map URL if it is 404/403 (path typo, moved file)","For 429/5xx, retry after the rate limit or incident clears - the prober re-fetches each cycle","Self-host the derpmap JSON at a stable URL if the upstream is flaky"],"exampleFix":"// before\nprober, _ := NewDerpProber(logf, \"https://example.com/derpmap\", ..., ...)\n\n// after\nprober, _ := NewDerpProber(logf, \"https://example.com/derpmap/default.json\", ..., ...) // correct path returning 200","handlingStrategy":"retry","validationCode":"// Pre-flight the map URL before handing it to the prober\nresp, err := http.Head(derpMapURL)\nif err != nil || resp.StatusCode != 200 {\n\treturn fmt.Errorf(\"derp map URL unhealthy (%v, status %v); using last known map\", err, resp.Status)\n}","typeGuard":null,"tryCatchPattern":"if err := prober.UpdateMap(ctx); err != nil {\n\tif strings.Contains(err.Error(), \"fetching\") {\n\t\t// non-200 from control: keep the previous map, retry next cycle\n\t\t// (transport errors already fall back to the cache inside updateMap)\n\t}\n}","preventionTips":["Monitor the derpMapURL with a plain HTTP up/down check alongside the prober","Self-host a mirror of the map if you need immunity from control-plane blips","Treat 429 seriously: back off rather than hammering the URL each probe cycle"],"tags":["derp","prober","http","derpmap","go"],"backgroundTag":"http-non-200-status","analyzedSha":"5201273aec737d6372ab7423c31c04ca3ca2a0c2","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}