[{"data":1,"prerenderedAt":1642},["ShallowReactive",2],{"docs-\u002Fdocs\u002Fgolang-quickstart":3,"docs-nav":1628},{"id":4,"title":5,"body":6,"description":1620,"extension":1621,"meta":1622,"navigation":144,"order":189,"path":1623,"section":1624,"seo":1625,"stem":1626,"__hash__":1627},"docs\u002Fdocs\u002Fgolang-quickstart.md","Go",{"type":7,"value":8,"toc":1603},"minimark",[9,14,18,21,33,37,65,69,96,107,118,122,129,279,282,296,308,315,320,331,346,353,357,360,385,407,441,445,448,451,474,477,490,493,502,509,515,519,526,549,560,574,578,592,595,609,627,630,657,664,670,674,677,684,772,775,779,794,812,818,825,834,837,847,921,994,1015,1061,1067,1147,1152,1172,1176,1183,1522,1529,1532,1555,1566,1570,1599],[10,11,13],"h2",{"id":12},"overview","Overview",[15,16,17],"p",{},"This guide adds BlockBots to a Go web server. You will get one module and wrap your mux — detection then runs on every request before your handlers do.",[15,19,20],{},"It takes about five minutes. Standard library only, no database, no build step.",[22,23,24],"blockquote",{},[15,25,26,27,32],{},"This guide covers server-side protection. If your pages are rendered entirely in the browser, see the ",[28,29,31],"a",{"href":30},"\u002Fdocs\u002Fjavascript-quickstart","JavaScript guide",". The two work together.",[10,34,36],{"id":35},"prerequisites","Prerequisites",[38,39,40,49,62],"ul",{},[41,42,43,44,48],"li",{},"Go ",[45,46,47],"strong",{},"1.21"," or higher",[41,50,51,52,58,59],{},"An API key — create one at ",[28,53,57],{"href":54,"rel":55},"https:\u002F\u002Fpanel.blockbots.org",[56],"nofollow","panel.blockbots.org"," under ",[45,60,61],{},"Settings → API Keys",[41,63,64],{},"The domain registered on that key, exactly as visitors reach it",[10,66,68],{"id":67},"step-1-install","Step 1 — Install",[70,71,77],"pre",{"className":72,"code":73,"filename":74,"language":75,"meta":76,"style":76},"language-bash shiki shiki-themes github-dark-dimmed","go get github.com\u002Fblockbots-org\u002Flibs-blockbots\u002Fgo\n","terminal","bash","",[78,79,80],"code",{"__ignoreMap":76},[81,82,85,89,93],"span",{"class":83,"line":84},"line",1,[81,86,88],{"style":87},"--shiki-default:#F69D50","go",[81,90,92],{"style":91},"--shiki-default:#96D0FF"," get",[81,94,95],{"style":91}," github.com\u002Fblockbots-org\u002Flibs-blockbots\u002Fgo\n",[70,97,101],{"className":98,"code":99,"filename":100,"language":88,"meta":76,"style":76},"language-go shiki shiki-themes github-dark-dimmed","import blockbots \"github.com\u002Fblockbots-org\u002Flibs-blockbots\u002Fgo\"\n","main.go",[78,102,103],{"__ignoreMap":76},[81,104,105],{"class":83,"line":84},[81,106,99],{},[15,108,109,110,113,114,117],{},"The import path ends in ",[78,111,112],{},"\u002Fgo"," but the package is named ",[78,115,116],{},"blockbots",", so the alias keeps it readable.",[10,119,121],{"id":120},"step-2-wrap-your-mux","Step 2 — Wrap your mux",[15,123,124,125,128],{},"Build the client once at startup and wrap the mux ",[45,126,127],{},"before"," it serves anything:",[70,130,132],{"className":98,"code":131,"filename":100,"language":88,"meta":76,"style":76},"package main\n\nimport (\n    \"log\"\n    \"net\u002Fhttp\"\n    \"os\"\n\n    blockbots \"github.com\u002Fblockbots-org\u002Flibs-blockbots\u002Fgo\"\n)\n\nfunc main() {\n    mux := http.NewServeMux()\n    mux.HandleFunc(\"\u002F\", func(w http.ResponseWriter, r *http.Request) {\n        w.Write([]byte(\"Welcome\"))\n    })\n\n    bb, err := blockbots.New(blockbots.Options{\n        APIKey: os.Getenv(\"BLOCKBOTS_API_KEY\"),\n    })\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    log.Fatal(http.ListenAndServe(\":8080\", bb.Handler(mux)))\n}\n",[78,133,134,139,146,152,158,164,170,175,181,187,192,198,204,210,216,222,227,233,239,244,250,256,262,267,273],{"__ignoreMap":76},[81,135,136],{"class":83,"line":84},[81,137,138],{},"package main\n",[81,140,142],{"class":83,"line":141},2,[81,143,145],{"emptyLinePlaceholder":144},true,"\n",[81,147,149],{"class":83,"line":148},3,[81,150,151],{},"import (\n",[81,153,155],{"class":83,"line":154},4,[81,156,157],{},"    \"log\"\n",[81,159,161],{"class":83,"line":160},5,[81,162,163],{},"    \"net\u002Fhttp\"\n",[81,165,167],{"class":83,"line":166},6,[81,168,169],{},"    \"os\"\n",[81,171,173],{"class":83,"line":172},7,[81,174,145],{"emptyLinePlaceholder":144},[81,176,178],{"class":83,"line":177},8,[81,179,180],{},"    blockbots \"github.com\u002Fblockbots-org\u002Flibs-blockbots\u002Fgo\"\n",[81,182,184],{"class":83,"line":183},9,[81,185,186],{},")\n",[81,188,190],{"class":83,"line":189},10,[81,191,145],{"emptyLinePlaceholder":144},[81,193,195],{"class":83,"line":194},11,[81,196,197],{},"func main() {\n",[81,199,201],{"class":83,"line":200},12,[81,202,203],{},"    mux := http.NewServeMux()\n",[81,205,207],{"class":83,"line":206},13,[81,208,209],{},"    mux.HandleFunc(\"\u002F\", func(w http.ResponseWriter, r *http.Request) {\n",[81,211,213],{"class":83,"line":212},14,[81,214,215],{},"        w.Write([]byte(\"Welcome\"))\n",[81,217,219],{"class":83,"line":218},15,[81,220,221],{},"    })\n",[81,223,225],{"class":83,"line":224},16,[81,226,145],{"emptyLinePlaceholder":144},[81,228,230],{"class":83,"line":229},17,[81,231,232],{},"    bb, err := blockbots.New(blockbots.Options{\n",[81,234,236],{"class":83,"line":235},18,[81,237,238],{},"        APIKey: os.Getenv(\"BLOCKBOTS_API_KEY\"),\n",[81,240,242],{"class":83,"line":241},19,[81,243,221],{},[81,245,247],{"class":83,"line":246},20,[81,248,249],{},"    if err != nil {\n",[81,251,253],{"class":83,"line":252},21,[81,254,255],{},"        log.Fatal(err)\n",[81,257,259],{"class":83,"line":258},22,[81,260,261],{},"    }\n",[81,263,265],{"class":83,"line":264},23,[81,266,145],{"emptyLinePlaceholder":144},[81,268,270],{"class":83,"line":269},24,[81,271,272],{},"    log.Fatal(http.ListenAndServe(\":8080\", bb.Handler(mux)))\n",[81,274,276],{"class":83,"line":275},25,[81,277,278],{},"}\n",[15,280,281],{},"There is nothing to call. The middleware decides on its own and answers the requests it stops.",[15,283,284,287,288,291,292,295],{},[78,285,286],{},"New"," returns an error for exactly one reason — no API key, from the option or from the environment. ",[78,289,290],{},"BLOCKBOTS_API_KEY"," is picked up automatically, so ",[78,293,294],{},"blockbots.New(blockbots.Options{})"," also works when that variable is set.",[22,297,298],{},[15,299,300,303,304,307],{},[45,301,302],{},"Order matters."," A wrapper applied to one route never sees a request another route has already answered, so nothing is protected. Wrap the mux, not a handler inside it — this is the Go equivalent of including the PHP file after the first ",[78,305,306],{},"echo",".",[15,309,310,311,314],{},"A ",[78,312,313],{},"Client"," is safe for any number of goroutines and is meant to be built once, at startup, not per request. It holds the verdict cache and a pooled HTTP client; building one per request throws both away.",[316,317,319],"h3",{"id":318},"chi-gorillamux-alice","chi, gorilla\u002Fmux, alice",[15,321,322,323,326,327,330],{},"Once bound, ",[78,324,325],{},"bb.Handler"," is a plain ",[78,328,329],{},"func(http.Handler) http.Handler",", so it drops straight into anything that speaks that shape:",[70,332,334],{"className":98,"code":333,"filename":100,"language":88,"meta":76,"style":76},"r := chi.NewRouter()\nr.Use(bb.Handler)\n",[78,335,336,341],{"__ignoreMap":76},[81,337,338],{"class":83,"line":84},[81,339,340],{},"r := chi.NewRouter()\n",[81,342,343],{"class":83,"line":141},[81,344,345],{},"r.Use(bb.Handler)\n",[15,347,348,349,307],{},"For Gin, Echo and Fiber — whose handler signatures are their own — see ",[28,350,352],{"href":351},"#making-the-decision-yourself","Making the decision yourself",[10,354,356],{"id":355},"step-3-exclude-what-must-not-be-checked","Step 3 — Exclude what must not be checked",[15,358,359],{},"Health checks, webhooks, your admin area and your own API routes should go straight through:",[70,361,363],{"className":98,"code":362,"filename":100,"language":88,"meta":76,"style":76},"bb, err := blockbots.New(blockbots.Options{\n    APIKey:  os.Getenv(\"BLOCKBOTS_API_KEY\"),\n    Exclude: []string{\"\u002Fhealthz\", \"\u002Fwebhooks\", \"\u002Fapi\", \"\u002Fadmin\"},\n})\n",[78,364,365,370,375,380],{"__ignoreMap":76},[81,366,367],{"class":83,"line":84},[81,368,369],{},"bb, err := blockbots.New(blockbots.Options{\n",[81,371,372],{"class":83,"line":141},[81,373,374],{},"    APIKey:  os.Getenv(\"BLOCKBOTS_API_KEY\"),\n",[81,376,377],{"class":83,"line":148},[81,378,379],{},"    Exclude: []string{\"\u002Fhealthz\", \"\u002Fwebhooks\", \"\u002Fapi\", \"\u002Fadmin\"},\n",[81,381,382],{"class":83,"line":154},[81,383,384],{},"})\n",[15,386,387,388,391,392,394,395,398,399,402,403,406],{},"A prefix matches that path and everything under it — ",[78,389,390],{},"\u002Fapi"," covers ",[78,393,390],{}," and ",[78,396,397],{},"\u002Fapi\u002Fusers",", but not ",[78,400,401],{},"\u002Fapi-docs",". For what a prefix cannot express, use ",[78,404,405],{},"ExcludeFunc",":",[70,408,410],{"className":98,"code":409,"filename":100,"language":88,"meta":76,"style":76},"blockbots.Options{\n    Exclude: []string{\"\u002Fwebhooks\u002Fstripe\"},\n    ExcludeFunc: func(r *http.Request) bool {\n        return r.Header.Get(\"X-Internal-Token\") == internalToken\n    },\n}\n",[78,411,412,417,422,427,432,437],{"__ignoreMap":76},[81,413,414],{"class":83,"line":84},[81,415,416],{},"blockbots.Options{\n",[81,418,419],{"class":83,"line":141},[81,420,421],{},"    Exclude: []string{\"\u002Fwebhooks\u002Fstripe\"},\n",[81,423,424],{"class":83,"line":148},[81,425,426],{},"    ExcludeFunc: func(r *http.Request) bool {\n",[81,428,429],{"class":83,"line":154},[81,430,431],{},"        return r.Header.Get(\"X-Internal-Token\") == internalToken\n",[81,433,434],{"class":83,"line":160},[81,435,436],{},"    },\n",[81,438,439],{"class":83,"line":166},[81,440,278],{},[10,442,444],{"id":443},"step-4-check-it-works","Step 4 — Check it works",[15,446,447],{},"Open your site in a private window. You should see a verification screen for a second, then your own page.",[15,449,450],{},"To confirm what is going on under the hood, switch on the self-test:",[70,452,454],{"className":98,"code":453,"filename":100,"language":88,"meta":76,"style":76},"blockbots.Options{\n    APIKey:   os.Getenv(\"BLOCKBOTS_API_KEY\"),\n    Selftest: \"pick-a-long-secret\",\n}\n",[78,455,456,460,465,470],{"__ignoreMap":76},[81,457,458],{"class":83,"line":84},[81,459,416],{},[81,461,462],{"class":83,"line":141},[81,463,464],{},"    APIKey:   os.Getenv(\"BLOCKBOTS_API_KEY\"),\n",[81,466,467],{"class":83,"line":148},[81,468,469],{},"    Selftest: \"pick-a-long-secret\",\n",[81,471,472],{"class":83,"line":154},[81,473,278],{},[15,475,476],{},"Then request any page with that secret:",[70,478,480],{"className":72,"code":479,"filename":74,"language":75,"meta":76,"style":76},"curl \"https:\u002F\u002Fyour-site.com\u002F?bb-selftest=pick-a-long-secret\"\n",[78,481,482],{"__ignoreMap":76},[81,483,484,487],{"class":83,"line":84},[81,485,486],{"style":87},"curl",[81,488,489],{"style":91}," \"https:\u002F\u002Fyour-site.com\u002F?bb-selftest=pick-a-long-secret\"\n",[15,491,492],{},"A healthy install answers like this:",[70,494,500],{"className":495,"code":497,"filename":498,"language":499,"meta":76},[496],"language-text","Blockbots self-test\n\nenvironment\n  go                     go1.22.3\n  platform               generic (has a socket)\n  ed25519                yes\n  library active         yes\n  disabled option        false\n  service state          ok\n\nthis request\n  socket address         203.0.113.9\n  trusted proxy          NO\n  resolved client ip     203.0.113.9\n  ip came from           socket\n  trusted proxy ranges   0 configured\n  looks like a browser   yes\n  navigation             yes\n  host                   your-site.com\n  path excluded          NO\n\ntoken\n  __bbjwt cookie         absent - visitor has not been verified yet\n\ncache\n  store                  memory\n  entries                14\n  ttl                    3600s\n\napi\n  url                    https:\u002F\u002Fapi.blockbots.org\u002Fv1\u002Fverify-bot\n  round trip             86 ms\n  reachable              yes\n  answered               not_bot\n","output","text",[78,501,497],{"__ignoreMap":76},[15,503,504,505,508],{},"Nothing secret is printed — not your API key, not the visitor's token. The secret is compared in constant time. Leave ",[78,506,507],{},"Selftest"," unset and the page does not exist.",[15,510,511,514],{},[78,512,513],{},"ip came from"," is the line that explains a verification loop.",[10,516,518],{"id":517},"step-5-if-you-use-a-cdn-or-reverse-proxy","Step 5 — If you use a CDN or reverse proxy",[15,520,521,522,525],{},"Cloudflare is recognised automatically, and so are nginx, Docker and a load balancer on your own network. ",[45,523,524],{},"Any other CDN needs one line",", otherwise every visitor arrives as the same address and nobody can pass verification:",[70,527,529],{"className":98,"code":528,"filename":100,"language":88,"meta":76,"style":76},"blockbots.Options{\n    APIKey:         os.Getenv(\"BLOCKBOTS_API_KEY\"),\n    TrustedProxies: \"203.0.113.0\u002F24,198.51.100.7\",\n}\n",[78,530,531,535,540,545],{"__ignoreMap":76},[81,532,533],{"class":83,"line":84},[81,534,416],{},[81,536,537],{"class":83,"line":141},[81,538,539],{},"    APIKey:         os.Getenv(\"BLOCKBOTS_API_KEY\"),\n",[81,541,542],{"class":83,"line":148},[81,543,544],{},"    TrustedProxies: \"203.0.113.0\u002F24,198.51.100.7\",\n",[81,546,547],{"class":83,"line":154},[81,548,278],{},[15,550,551,552,555,556,559],{},"Use the address ranges your CDN connects from — your provider publishes them. Addresses and CIDR blocks both work, comma separated, and ",[78,553,554],{},"BLOCKBOTS_TRUSTED_PROXIES"," is read if the option is empty. ",[78,557,558],{},"X-Forwarded-For"," is parsed right to left, skipping your own hops.",[15,561,562,563,566,567,394,570,573],{},"In the self-test, ",[78,564,565],{},"trusted proxy"," should read ",[78,568,569],{},"yes",[78,571,572],{},"resolved client ip"," should be the visitor's real address, not your CDN's.",[10,575,577],{"id":576},"step-6-serverless","Step 6 — Serverless",[15,579,580,583,584,587,588,591],{},[78,581,582],{},"net\u002Fhttp"," always fills ",[78,585,586],{},"RemoteAddr",", so this is a narrower problem in Go than elsewhere — it only arises in a shim that builds an ",[78,589,590],{},"*http.Request"," from an event.",[15,593,594],{},"Vercel, Netlify and Deno Deploy are detected from their own environment variables and need nothing.",[15,596,597,600,601,605,606,608],{},[45,598,599],{},"AWS Lambda behind API Gateway and Google Cloud Run are deliberately not detected",", unlike in the Python and Node clients' serverless paths. Both ",[602,603,604],"em",{},"append"," to ",[78,607,558],{}," rather than overwriting it, and which end of that chain is spoofable is a judgement call this library will not make for you. Name the source yourself:",[70,610,612],{"className":98,"code":611,"filename":100,"language":88,"meta":76,"style":76},"blockbots.Options{\n    ClientIPHeader: \"X-Real-Ip\",\n}\n",[78,613,614,618,623],{"__ignoreMap":76},[81,615,616],{"class":83,"line":84},[81,617,416],{},[81,619,620],{"class":83,"line":141},[81,621,622],{},"    ClientIPHeader: \"X-Real-Ip\",\n",[81,624,625],{"class":83,"line":148},[81,626,278],{},[15,628,629],{},"Or compute it, when only your code knows the shape of the event:",[70,631,633],{"className":98,"code":632,"filename":100,"language":88,"meta":76,"style":76},"blockbots.Options{\n    ClientIP: func(r *http.Request) string {\n        return r.Header.Get(\"X-Forwarded-For\") \u002F\u002F your own parsing\n    },\n}\n",[78,634,635,639,644,649,653],{"__ignoreMap":76},[81,636,637],{"class":83,"line":84},[81,638,416],{},[81,640,641],{"class":83,"line":141},[81,642,643],{},"    ClientIP: func(r *http.Request) string {\n",[81,645,646],{"class":83,"line":148},[81,647,648],{},"        return r.Header.Get(\"X-Forwarded-For\") \u002F\u002F your own parsing\n",[81,650,651],{"class":83,"line":154},[81,652,436],{},[81,654,655],{"class":83,"line":160},[81,656,278],{},[15,658,659,660,663],{},"Why it is not guessed: with no verifiable peer to check a header against, trusting one would let any visitor choose their own address with a single ",[78,661,662],{},"curl -H"," flag, which defeats the verdict cache and the token's IP binding at once.",[15,665,666,667,307],{},"Without an address, protection switches itself off for those requests and says so in your log — deliberately, because a token with no address to bind to would send every visitor back to the verification page forever. The reason appears as ",[78,668,669],{},"ip_unresolved",[10,671,673],{"id":672},"step-7-running-more-than-one-instance","Step 7 — Running more than one instance",[15,675,676],{},"Nothing to do, but worth knowing: the default verdict cache is an in-process map, and each instance has its own. A crawler verdict is learned once per instance rather than once per site, which costs a few extra cloud calls and nothing else.",[15,678,679,680,683],{},"To share them, pass a ",[78,681,682],{},"Store",". It is two methods, so a Redis adapter is short:",[70,685,688],{"className":98,"code":686,"filename":687,"language":88,"meta":76,"style":76},"type redisStore struct{ rdb *redis.Client }\n\nfunc (s redisStore) Get(ctx context.Context, key string) (string, bool, error) {\n    v, err := s.rdb.Get(ctx, key).Result()\n    if err == redis.Nil {\n        return \"\", false, nil \u002F\u002F a miss is not an error\n    }\n    return v, err == nil, err\n}\n\nfunc (s redisStore) Set(ctx context.Context, key, value string, ttl time.Duration) error {\n    return s.rdb.Set(ctx, key, value, ttl).Err()\n}\n\nbb, err := blockbots.New(blockbots.Options{\n    APIKey: os.Getenv(\"BLOCKBOTS_API_KEY\"),\n    Store:  redisStore{rdb: rdb},\n})\n","store.go",[78,689,690,695,699,704,709,714,719,723,728,732,736,741,746,750,754,758,763,768],{"__ignoreMap":76},[81,691,692],{"class":83,"line":84},[81,693,694],{},"type redisStore struct{ rdb *redis.Client }\n",[81,696,697],{"class":83,"line":141},[81,698,145],{"emptyLinePlaceholder":144},[81,700,701],{"class":83,"line":148},[81,702,703],{},"func (s redisStore) Get(ctx context.Context, key string) (string, bool, error) {\n",[81,705,706],{"class":83,"line":154},[81,707,708],{},"    v, err := s.rdb.Get(ctx, key).Result()\n",[81,710,711],{"class":83,"line":160},[81,712,713],{},"    if err == redis.Nil {\n",[81,715,716],{"class":83,"line":166},[81,717,718],{},"        return \"\", false, nil \u002F\u002F a miss is not an error\n",[81,720,721],{"class":83,"line":172},[81,722,261],{},[81,724,725],{"class":83,"line":177},[81,726,727],{},"    return v, err == nil, err\n",[81,729,730],{"class":83,"line":183},[81,731,278],{},[81,733,734],{"class":83,"line":189},[81,735,145],{"emptyLinePlaceholder":144},[81,737,738],{"class":83,"line":194},[81,739,740],{},"func (s redisStore) Set(ctx context.Context, key, value string, ttl time.Duration) error {\n",[81,742,743],{"class":83,"line":200},[81,744,745],{},"    return s.rdb.Set(ctx, key, value, ttl).Err()\n",[81,747,748],{"class":83,"line":206},[81,749,278],{},[81,751,752],{"class":83,"line":212},[81,753,145],{"emptyLinePlaceholder":144},[81,755,756],{"class":83,"line":218},[81,757,369],{},[81,759,760],{"class":83,"line":224},[81,761,762],{},"    APIKey: os.Getenv(\"BLOCKBOTS_API_KEY\"),\n",[81,764,765],{"class":83,"line":229},[81,766,767],{},"    Store:  redisStore{rdb: rdb},\n",[81,769,770],{"class":83,"line":235},[81,771,384],{},[15,773,774],{},"A store that returns an error is logged and treated as a miss, so a Redis outage costs cloud calls, not requests. Keys are shared with the PHP, Node and Python clients, so one Redis can serve a mixed estate.",[10,776,778],{"id":777},"how-it-works","How it works",[780,781,782,785,791],"ol",{},[41,783,784],{},"A visitor with a valid cookie passes through instantly — no network call.",[41,786,787,788,307],{},"Anything that does not look like a browser is checked against the cloud. Crawlers you allow, such as Googlebot, get your content; impostors using their name get ",[78,789,790],{},"403",[41,792,793],{},"Everyone else sees the verification page, gets a signed cookie, and the page reloads.",[15,795,796,797,394,800,803,804,807,808,811],{},"Verdicts are cached, so a crawler costs one cloud call per hour rather than one per page. ",[78,798,799],{},"fetch",[78,801,802],{},"XHR"," requests are never interrupted, and ",[78,805,806],{},"POST"," bodies are never swallowed. ",[78,809,810],{},"Check"," uses the request's own context, so a cancelled request abandons the cloud call with it. If the cloud is unreachable, the middleware steps aside and your site keeps serving.",[15,813,814,817],{},[45,815,816],{},"A dead or expired key never reaches your visitors."," Before the verification page is served for the first time, BlockBots asks whether the cloud is serving your key at all. A rejection switches protection off for five minutes rather than handing out a page whose script would fail the same way and never reload — so a lapsed subscription leaves your site unprotected, not broken. The answer is cached, so this costs one request an hour, not one per visitor, and renewing a subscription takes effect on its own with no restart.",[15,819,820,821,824],{},"Everything goes to ",[78,822,823],{},"slog.Default()"," — nothing is printed and nothing panics. Hand over your own logger to route it:",[70,826,828],{"className":98,"code":827,"filename":100,"language":88,"meta":76,"style":76},"blockbots.Options{Logger: slog.New(handler)}\n",[78,829,830],{"__ignoreMap":76},[81,831,832],{"class":83,"line":84},[81,833,827],{},[10,835,352],{"id":836},"making-the-decision-yourself",[15,838,839,840,843,844,846],{},"For anything ",[78,841,842],{},"Handler"," does not cover — or when you would rather log than block — ",[78,845,810],{}," reads a request and returns a verdict without touching the response:",[70,848,851],{"className":98,"code":849,"filename":850,"language":88,"meta":76,"style":76},"d := bb.Check(r)\n\u002F\u002F blockbots.Decision{\n\u002F\u002F   Action: blockbots.ActionChallenge,\n\u002F\u002F   Allow:  false,\n\u002F\u002F   Reason: \"unverified\",\n\u002F\u002F   IP:     \"203.0.113.9\",\n\u002F\u002F   UA:     \"Mozilla\u002F5.0 …\",\n\u002F\u002F   Page:   *blockbots.Page,\n\u002F\u002F }\n\nif !d.Allow {\n    d.Page.WriteTo(w, r)\n    return\n}\n","protect.go",[78,852,853,858,863,868,873,878,883,888,893,898,902,907,912,917],{"__ignoreMap":76},[81,854,855],{"class":83,"line":84},[81,856,857],{},"d := bb.Check(r)\n",[81,859,860],{"class":83,"line":141},[81,861,862],{},"\u002F\u002F blockbots.Decision{\n",[81,864,865],{"class":83,"line":148},[81,866,867],{},"\u002F\u002F   Action: blockbots.ActionChallenge,\n",[81,869,870],{"class":83,"line":154},[81,871,872],{},"\u002F\u002F   Allow:  false,\n",[81,874,875],{"class":83,"line":160},[81,876,877],{},"\u002F\u002F   Reason: \"unverified\",\n",[81,879,880],{"class":83,"line":166},[81,881,882],{},"\u002F\u002F   IP:     \"203.0.113.9\",\n",[81,884,885],{"class":83,"line":172},[81,886,887],{},"\u002F\u002F   UA:     \"Mozilla\u002F5.0 …\",\n",[81,889,890],{"class":83,"line":177},[81,891,892],{},"\u002F\u002F   Page:   *blockbots.Page,\n",[81,894,895],{"class":83,"line":183},[81,896,897],{},"\u002F\u002F }\n",[81,899,900],{"class":83,"line":189},[81,901,145],{"emptyLinePlaceholder":144},[81,903,904],{"class":83,"line":194},[81,905,906],{},"if !d.Allow {\n",[81,908,909],{"class":83,"line":200},[81,910,911],{},"    d.Page.WriteTo(w, r)\n",[81,913,914],{"class":83,"line":206},[81,915,916],{},"    return\n",[81,918,919],{"class":83,"line":212},[81,920,278],{},[922,923,924,942],"table",{},[925,926,927],"thead",{},[928,929,930,936,939],"tr",{},[931,932,933],"th",{},[78,934,935],{},"Action",[931,937,938],{},"Meaning",[931,940,941],{},"What to do",[943,944,945,959,979],"tbody",{},[928,946,947,953,956],{},[948,949,950],"td",{},[78,951,952],{},"ActionAllow",[948,954,955],{},"Let it through",[948,957,958],{},"Call your handler",[928,960,961,966,969],{},[948,962,963],{},[78,964,965],{},"ActionBlock",[948,967,968],{},"A bot failed its check",[948,970,971,972,975,976,978],{},"Send ",[78,973,974],{},"Page"," — a ",[78,977,790],{}," page",[928,980,981,986,989],{},[948,982,983],{},[78,984,985],{},"ActionChallenge",[948,987,988],{},"Unverified visitor",[948,990,971,991,993],{},[78,992,974],{}," — the verification page, which reloads once passed",[15,995,996,998,999,1002,1003,1006,1007,1010,1011,1014],{},[78,997,974],{}," is ",[78,1000,1001],{},"{Status int, Header http.Header, Body string}",", so a framework that writes the response itself can do that instead of calling ",[78,1004,1005],{},"WriteTo",". It is ",[78,1008,1009],{},"nil"," whenever ",[78,1012,1013],{},"Allow"," is true.",[15,1016,1017,1020,1021,1024,1025,1024,1028,1024,1031,1024,1034,1024,1037,1024,1040,1024,1043,1024,1045,1024,1048,1024,1051,1024,1054,1057,1058,307],{},[78,1018,1019],{},"Reason"," is for your logs: ",[78,1022,1023],{},"token_valid",", ",[78,1026,1027],{},"verified_bot",[78,1029,1030],{},"cached_allowed",[78,1032,1033],{},"cached_disallowed",[78,1035,1036],{},"not_a_navigation",[78,1038,1039],{},"excluded",[78,1041,1042],{},"unverified",[78,1044,669],{},[78,1046,1047],{},"unauthorized",[78,1049,1050],{},"service_disabled",[78,1052,1053],{},"disabled",[78,1055,1056],{},"api_*"," when the cloud could not be reached, or the cloud's own reason for blocking such as ",[78,1059,1060],{},"fake_googlebot",[15,1062,1063,1064,1066],{},"Remember the self-test, which ",[78,1065,842],{}," would otherwise have served for you:",[70,1068,1070],{"className":98,"code":1069,"filename":850,"language":88,"meta":76,"style":76},"\u002F\u002F Gin\nfunc Protect(bb *blockbots.Client) gin.HandlerFunc {\n    return func(c *gin.Context) {\n        if bb.IsSelftestRequest(c.Request) {\n            bb.Selftest(c.Request).WriteTo(c.Writer, c.Request)\n            c.Abort()\n            return\n        }\n        if d := bb.Check(c.Request); !d.Allow {\n            d.Page.WriteTo(c.Writer, c.Request)\n            c.Abort()\n            return\n        }\n        c.Next()\n    }\n}\n",[78,1071,1072,1077,1082,1087,1092,1097,1102,1107,1112,1117,1122,1126,1130,1134,1139,1143],{"__ignoreMap":76},[81,1073,1074],{"class":83,"line":84},[81,1075,1076],{},"\u002F\u002F Gin\n",[81,1078,1079],{"class":83,"line":141},[81,1080,1081],{},"func Protect(bb *blockbots.Client) gin.HandlerFunc {\n",[81,1083,1084],{"class":83,"line":148},[81,1085,1086],{},"    return func(c *gin.Context) {\n",[81,1088,1089],{"class":83,"line":154},[81,1090,1091],{},"        if bb.IsSelftestRequest(c.Request) {\n",[81,1093,1094],{"class":83,"line":160},[81,1095,1096],{},"            bb.Selftest(c.Request).WriteTo(c.Writer, c.Request)\n",[81,1098,1099],{"class":83,"line":166},[81,1100,1101],{},"            c.Abort()\n",[81,1103,1104],{"class":83,"line":172},[81,1105,1106],{},"            return\n",[81,1108,1109],{"class":83,"line":177},[81,1110,1111],{},"        }\n",[81,1113,1114],{"class":83,"line":183},[81,1115,1116],{},"        if d := bb.Check(c.Request); !d.Allow {\n",[81,1118,1119],{"class":83,"line":189},[81,1120,1121],{},"            d.Page.WriteTo(c.Writer, c.Request)\n",[81,1123,1124],{"class":83,"line":194},[81,1125,1101],{},[81,1127,1128],{"class":83,"line":200},[81,1129,1106],{},[81,1131,1132],{"class":83,"line":206},[81,1133,1111],{},[81,1135,1136],{"class":83,"line":212},[81,1137,1138],{},"        c.Next()\n",[81,1140,1141],{"class":83,"line":218},[81,1142,261],{},[81,1144,1145],{"class":83,"line":224},[81,1146,278],{},[15,1148,1149,1151],{},[78,1150,842],{}," leaves the decision in the request context, so a handler behind it can read what happened without doing the work again:",[70,1153,1156],{"className":98,"code":1154,"filename":1155,"language":88,"meta":76,"style":76},"if d, ok := blockbots.DecisionFrom(r); ok {\n    log.Print(d.Reason, d.IP)\n}\n","handler.go",[78,1157,1158,1163,1168],{"__ignoreMap":76},[81,1159,1160],{"class":83,"line":84},[81,1161,1162],{},"if d, ok := blockbots.DecisionFrom(r); ok {\n",[81,1164,1165],{"class":83,"line":141},[81,1166,1167],{},"    log.Print(d.Reason, d.IP)\n",[81,1169,1170],{"class":83,"line":148},[81,1171,278],{},[10,1173,1175],{"id":1174},"configuration","Configuration",[15,1177,1178,1179,1182],{},"Every field's zero value is a working default, so only ",[78,1180,1181],{},"APIKey"," is required — and even that may come from the environment.",[922,1184,1185,1198],{},[925,1186,1187],{},[928,1188,1189,1192,1195],{},[931,1190,1191],{},"Option",[931,1193,1194],{},"Default",[931,1196,1197],{},"What it does",[943,1199,1200,1217,1233,1249,1267,1281,1296,1311,1326,1341,1356,1370,1389,1404,1419,1432,1444,1456,1467,1481,1493,1508],{},[928,1201,1202,1206,1211],{},[948,1203,1204],{},[78,1205,1181],{},[948,1207,1208],{},[78,1209,1210],{},"$BLOCKBOTS_API_KEY",[948,1212,1213,1216],{},[45,1214,1215],{},"Required."," Your API key",[928,1218,1219,1224,1230],{},[948,1220,1221],{},[78,1222,1223],{},"APIURL",[948,1225,1226,1229],{},[78,1227,1228],{},"$BLOCKBOTS_API_URL",", else production",[948,1231,1232],{},"The verify-bot endpoint",[928,1234,1235,1240,1246],{},[948,1236,1237],{},[78,1238,1239],{},"SDKURL",[948,1241,1242,1243],{},"jsDelivr ",[78,1244,1245],{},"@latest",[948,1247,1248],{},"Browser SDK the verification page imports",[928,1250,1251,1256,1261],{},[948,1252,1253],{},[78,1254,1255],{},"Disabled",[948,1257,1258],{},[78,1259,1260],{},"false",[948,1262,1263,1266],{},[78,1264,1265],{},"true"," turns protection off, middleware still mounted",[928,1268,1269,1274,1278],{},[948,1270,1271],{},[78,1272,1273],{},"Debug",[948,1275,1276],{},[78,1277,1260],{},[948,1279,1280],{},"Skip the token's IP check — for local development",[928,1282,1283,1288,1293],{},[948,1284,1285],{},[78,1286,1287],{},"Retries",[948,1289,1290],{},[78,1291,1292],{},"1",[948,1294,1295],{},"Verification attempts in the browser",[928,1297,1298,1303,1308],{},[948,1299,1300],{},[78,1301,1302],{},"Delay",[948,1304,1305],{},[78,1306,1307],{},"1s",[948,1309,1310],{},"Between attempts",[928,1312,1313,1318,1323],{},[948,1314,1315],{},[78,1316,1317],{},"Timeout",[948,1319,1320],{},[78,1321,1322],{},"1.5s",[948,1324,1325],{},"One cloud call, before the request is let through",[928,1327,1328,1333,1338],{},[948,1329,1330],{},[78,1331,1332],{},"CacheTTL",[948,1334,1335],{},[78,1336,1337],{},"1h",[948,1339,1340],{},"How long a crawler verdict is cached",[928,1342,1343,1348,1353],{},[948,1344,1345],{},[78,1346,1347],{},"CacheMax",[948,1349,1350],{},[78,1351,1352],{},"10000",[948,1354,1355],{},"Verdicts held per instance before the oldest is dropped",[928,1357,1358,1363,1367],{},[948,1359,1360],{},[78,1361,1362],{},"KeyCheckTTL",[948,1364,1365],{},[78,1366,1337],{},[948,1368,1369],{},"How long a confirmed-good API key is trusted",[928,1371,1372,1377,1382],{},[948,1373,1374],{},[78,1375,1376],{},"ChallengeStatus",[948,1378,1379],{},[78,1380,1381],{},"200",[948,1383,1384,1385,1388],{},"Use ",[78,1386,1387],{},"503"," to have search engines treat the verification page as a temporary hold",[928,1390,1391,1396,1401],{},[948,1392,1393],{},[78,1394,1395],{},"CookieName",[948,1397,1398],{},[78,1399,1400],{},"__bbjwt",[948,1402,1403],{},"Cookie the browser SDK sets",[928,1405,1406,1411,1416],{},[948,1407,1408],{},[78,1409,1410],{},"TrustedProxies",[948,1412,1413],{},[78,1414,1415],{},"$BLOCKBOTS_TRUSTED_PROXIES",[948,1417,1418],{},"Your CDN or load balancer addresses and CIDRs, comma separated",[928,1420,1421,1426,1429],{},[948,1422,1423],{},[78,1424,1425],{},"ClientIPHeader",[948,1427,1428],{},"—",[948,1430,1431],{},"Header to read the client address from",[928,1433,1434,1439,1441],{},[948,1435,1436],{},[78,1437,1438],{},"ClientIP",[948,1440,1428],{},[948,1442,1443],{},"Function computing it outright",[928,1445,1446,1451,1453],{},[948,1447,1448],{},[78,1449,1450],{},"Exclude",[948,1452,1428],{},[948,1454,1455],{},"Path prefixes left unchecked",[928,1457,1458,1462,1464],{},[948,1459,1460],{},[78,1461,405],{},[948,1463,1428],{},[948,1465,1466],{},"Your own rule for what to leave unchecked",[928,1468,1469,1473,1478],{},[948,1470,1471],{},[78,1472,507],{},[948,1474,1475],{},[78,1476,1477],{},"$BLOCKBOTS_SELFTEST",[948,1479,1480],{},"Secret that enables the self-test page",[928,1482,1483,1487,1490],{},[948,1484,1485],{},[78,1486,682],{},[948,1488,1489],{},"in-process",[948,1491,1492],{},"Shared verdict cache",[928,1494,1495,1500,1505],{},[948,1496,1497],{},[78,1498,1499],{},"HTTPClient",[948,1501,1502],{},[78,1503,1504],{},"&http.Client{}",[948,1506,1507],{},"Used for the cloud call",[928,1509,1510,1515,1519],{},[948,1511,1512],{},[78,1513,1514],{},"Logger",[948,1516,1517],{},[78,1518,823],{},[948,1520,1521],{},"Receives the conditions that make an install look protected while it is not",[15,1523,1524,1525,1528],{},"Unlike the PHP and Node clients, there is no degraded crypto mode to guard: ",[78,1526,1527],{},"crypto\u002Fed25519"," is pure Go and present in every build. The library still verifies a signature of its own at startup, so the fail-closed rule stays expressed in code — if that ever fails, protection disables itself rather than accepting tokens it has not checked.",[15,1530,1531],{},"For local development the cleanest switch is to turn it off:",[70,1533,1535],{"className":98,"code":1534,"filename":100,"language":88,"meta":76,"style":76},"blockbots.Options{\n    APIKey:   key,\n    Disabled: os.Getenv(\"ENV\") != \"production\",\n}\n",[78,1536,1537,1541,1546,1551],{"__ignoreMap":76},[81,1538,1539],{"class":83,"line":84},[81,1540,416],{},[81,1542,1543],{"class":83,"line":141},[81,1544,1545],{},"    APIKey:   key,\n",[81,1547,1548],{"class":83,"line":148},[81,1549,1550],{},"    Disabled: os.Getenv(\"ENV\") != \"production\",\n",[81,1552,1553],{"class":83,"line":154},[81,1554,278],{},[15,1556,1557,1558,1561,1562,1565],{},"Your API key is tied to its registered domain, so the cloud rejects requests from ",[78,1559,1560],{},"localhost"," anyway. ",[78,1563,1564],{},"Debug: true"," relaxes the token's IP check if you do want to test the real flow; never use it in production.",[10,1567,1569],{"id":1568},"next-steps","Next steps",[38,1571,1572,1578,1585,1592],{},[41,1573,1574,1577],{},[28,1575,1576],{"href":30},"JavaScript"," — add the browser SDK alongside this",[41,1579,1580,1584],{},[28,1581,1583],{"href":1582},"\u002Fdocs\u002Fnodejs-quickstart","Node.js"," — the same protection for an Express app",[41,1586,1587,1591],{},[28,1588,1590],{"href":1589},"\u002Fdocs\u002Fpython-quickstart","Python"," — the same protection for Django or FastAPI",[41,1593,1594,1598],{},[28,1595,1597],{"href":1596},"\u002Fdocs\u002Fphp-quickstart","PHP"," — the same protection for a PHP site",[1600,1601,1602],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":76,"searchDepth":141,"depth":141,"links":1604},[1605,1606,1607,1608,1611,1612,1613,1614,1615,1616,1617,1618,1619],{"id":12,"depth":141,"text":13},{"id":35,"depth":141,"text":36},{"id":67,"depth":141,"text":68},{"id":120,"depth":141,"text":121,"children":1609},[1610],{"id":318,"depth":148,"text":319},{"id":355,"depth":141,"text":356},{"id":443,"depth":141,"text":444},{"id":517,"depth":141,"text":518},{"id":576,"depth":141,"text":577},{"id":672,"depth":141,"text":673},{"id":777,"depth":141,"text":778},{"id":836,"depth":141,"text":352},{"id":1174,"depth":141,"text":1175},{"id":1568,"depth":141,"text":1569},"Protect a Go site from bots with one net\u002Fhttp middleware — standard library only, no dependencies.","md",{},"\u002Fdocs\u002Fgolang-quickstart","Server",{"title":5,"description":1620},"docs\u002Fgolang-quickstart","TiMefA2FFpK1la2_GSj_SUSpsYU73-kQVlOHnmwiz08",[1629,1633,1635,1636,1639,1640,1641],{"title":1630,"path":1631,"order":84,"section":1632},"Getting Started","\u002Fdocs\u002Fgetting-started","Getting started",{"title":1576,"path":30,"order":141,"section":1634},"Web",{"title":1597,"path":1596,"order":154,"section":1624},{"title":1637,"path":1638,"order":160,"section":1624},"WordPress Plugin","\u002Fdocs\u002Fwordpress-plugin",{"title":1583,"path":1582,"order":172,"section":1624},{"title":1590,"path":1589,"order":183,"section":1624},{"title":5,"path":1623,"order":189,"section":1624},1794295237812]