{"record":{"id":"8e01261022e41e60","repo":"Tencent/WeKnora","slug":"unmarshal-block-w","errorCode":null,"errorMessage":"unmarshal block: %w","messagePattern":"unmarshal block: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/datasource/connector/notion/client.go","lineNumber":372,"sourceCode":"\t\treturn nil, fmt.Errorf(\"query database %s: not a data_source (%v) and not a database (%v)\", id, err, dbErr)\n\t}\n\tif info.DataSourceID == \"\" {\n\t\treturn nil, fmt.Errorf(\"database %s has no data sources\", id)\n\t}\n\treturn c.paginatePages(ctx, http.MethodPost, fmt.Sprintf(\"/v1/data_sources/%s/query\", info.DataSourceID))\n}\n\n// ResolveBlock re-fetches a single block to resolve file_upload URLs.\n// When a block contains a file_upload type, re-fetching it returns the resolved\n// download URL (temporary S3 signed URL, 1-hour expiry).\nfunc (c *notionClient) ResolveBlock(ctx context.Context, blockID string) (*notionBlock, error) {\n\trespBody, err := c.doRequest(ctx, http.MethodGet, \"/v1/blocks/\"+blockID, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar block notionBlock\n\tif err := json.Unmarshal(respBody, &block); err != nil {\n\t\treturn nil, fmt.Errorf(\"unmarshal block: %w\", err)\n\t}\n\treturn &block, nil\n}\n\nconst maxDownloadSize = 100 * 1024 * 1024 // 100MB — prevent OOM from oversized files\n\n// DownloadFile downloads a file from the given URL (typically an S3 signed URL).\n// Does not go through the rate limiter since it's not a Notion API call.\nfunc (c *notionClient) DownloadFile(ctx context.Context, fileURL string) ([]byte, error) {\n\tif err := utils.ValidateURLForSSRF(fileURL); err != nil {\n\t\treturn nil, fmt.Errorf(\"attachment URL rejected: %w\", err)\n\t}\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create download request: %w\", err)\n\t}\n\n\tvar lastErr error","sourceCodeStart":354,"sourceCodeEnd":390,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/datasource/connector/notion/client.go#L354-L390","documentation":"ResolveBlock re-fetches a single block via GET /v1/blocks/{blockID} and unmarshals the JSON body into a notionBlock struct. This error wraps the json.Unmarshal failure, meaning Notion returned a body that is not valid JSON or whose shape does not fit notionBlock (e.g. a non-object payload such as an HTML error page or proxy response). The underlying json error is chained via %w.","triggerScenarios":"GET /v1/blocks/{blockID} returns 200 with a body that fails to decode into notionBlock: truncated JSON, an HTML interstitial from a proxy/gateway, or a Notion schema field whose type no longer matches the Go struct (e.g. block.type or file upload fields changing type).","commonSituations":"Corporate proxy or API gateway injecting an error/login page; Notion API schema change after a version bump; corrupted response from a flaky network path where a partial body arrives with a 200 status.","solutions":["Log the raw respBody (truncated) alongside the wrapped error to see what Notion actually returned; if it is HTML, look for proxy/auth interception.","Re-run with a fresh request — a truncated body is often transient; ResolveBlock is already retried by resolveFileUploads paths in some setups.","Update the notionBlock struct to match the current Notion API version's block schema (check recently changed fields like file_upload).","Verify the request went to api.notion.com directly, not through an intercepting proxy (check HTTPS_PROXY / corporate gateway)."],"exampleFix":"null","handlingStrategy":"retry","validationCode":"if len(respBody) == 0 || respBody[0] != '{' {\n    return fmt.Errorf(\"unexpected non-JSON block response (%d bytes)\", len(respBody))\n}","typeGuard":"func isUnmarshalBlockError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"unmarshal block:\")\n}","tryCatchPattern":"block, err := client.ResolveBlock(ctx, blockID)\nif err != nil {\n    if isUnmarshalBlockError(err) {\n        // transient/corrupt body: retry once, then skip this block's file resolution\n        return resolveWithRetry(ctx, blockID, 1)\n    }\n    return err\n}","preventionTips":["Log a truncated raw response body on decode failures to distinguish proxies/auth pages from schema drift.","Keep the notionClient pinned to a Notion API version you have tested the block schema against.","Check for corporate proxies (HTTPS_PROXY) that can inject HTML into API responses."],"tags":["notion","json","unmarshal","http"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}