{"record":{"id":"2037b5f132d07f44","repo":"infiniflow/ragflow","slug":"main-must-return-a-value-use-null-for-an-empty","errorCode":null,"errorMessage":"main() must return a value. Use null for an empty result.","messagePattern":"main\\(\\) must return a value\\. Use null for an empty result\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"agent/sandbox/result_protocol.py","lineNumber":50,"sourceCode":"    result = main(**{args_json})\n    payload = json.dumps({{\"present\": True, \"value\": result, \"type\": \"json\"}}, ensure_ascii=False, separators=(\",\", \":\"))\n    print(\"{RESULT_MARKER_PREFIX}\" + base64.b64encode(payload.encode(\"utf-8\")).decode(\"ascii\"))\n'''\n\n\ndef build_javascript_wrapper(code: str, args_json: str) -> str:\n    return f\"\"\"{code}\n\nconst __ragflowArgs = {args_json};\n\n(async () => {{\n  const __ragflowMain = typeof main !== 'undefined' ? main : module.exports && module.exports.main;\n  if (typeof __ragflowMain !== 'function') {{\n    throw new Error('main() must be defined or exported.');\n  }}\n  const output = await Promise.resolve(__ragflowMain(__ragflowArgs));\n  if (typeof output === 'undefined') {{\n    throw new Error('main() must return a value. Use null for an empty result.');\n  }}\n  const payload = JSON.stringify({{ present: true, value: output, type: 'json' }});\n  if (typeof payload === 'undefined') {{\n    throw new Error('main() returned a non-JSON-serializable value.');\n  }}\n  console.log('{RESULT_MARKER_PREFIX}' + Buffer.from(payload, 'utf8').toString('base64'));\n}})();\n\"\"\"\n\n\ndef extract_structured_result(stdout: str) -> tuple[str, dict[str, Any]]:\n    if not stdout:\n        return \"\", {}\n\n    cleaned_lines: list[str] = []\n    structured_result: dict[str, Any] = {}\n\n    for line in str(stdout).splitlines():","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/infiniflow/ragflow/blob/554fb1133ac3861732235ad9c377eb5e0a770665/agent/sandbox/result_protocol.py#L32-L68","documentation":"Raised when python-gitlab returns GitlabAuthorizationError (HTTP 403): the token authenticates fine but the authenticated user is not permitted to perform the action. In this validator it usually means the token cannot see the project owner/name being fetched. Maps to InsufficientPermissionsError.","triggerScenarios":"gitlab_client.auth() succeeds but projects.get(f\"{owner}/{name}\") returns 403 because the token's user has no Guest+ role on that (private) project, the token lacks the api scope (e.g. read_user only), or an IP-restriction/protected-branch policy blocks API access to the project.","commonSituations":"Using a read-only or narrowly-scoped PAT instead of one with api scope, project under a group the service account was never invited to, group-level IP allowlist that excludes the indexer's egress IP.","solutions":["Give the token's user at least Reporter role on the target project (or group)","Recreate the PAT with the api scope (or at minimum read_api + read_repository)","Check GitLab group Settings > Network > IP restrictions and allow the connector host","Verify the project path owner/name is the intended one — a wrong path on a private namespace also surfaces as 403"],"exampleFix":"# before: token only has read_user scope → auth ok, project get 403\ngl = gitlab.Gitlab(url, private_token=token)  # token scopes: read_user\n\n# after: mint token with proper scopes and grant project access\ngl = gitlab.Gitlab(url, private_token=token)  # token scopes: api, read_repository\n# then in GitLab: Project > Members > add token user as Reporter","handlingStrategy":"try-catch","validationCode":"import requests\n\ndef can_read_project(base_url: str, token: str, owner: str, name: str) -> bool:\n    r = requests.get(\n        f\"{base_url.rstrip('/')}/api/v4/projects/{quote(owner, safe='/')}%2F{name}\",\n        headers={\"PRIVATE-TOKEN\": token},\n        timeout=10,\n    )\n    return r.status_code == 200","typeGuard":"def token_has_needed_scopes(base_url: str, token: str) -> bool:\n    r = requests.get(f\"{base_url.rstrip('/')}/api/v4/personal_access_tokens/self\",\n                     headers={\"PRIVATE-TOKEN\": token}, timeout=10)\n    if r.status_code != 200:\n        return False\n    scopes = set(r.json().get(\"scopes\", []))\n    return \"api\" in scopes or {\"read_api\", \"read_repository\"} <= scopes","tryCatchPattern":"from common.data_source.exceptions import InsufficientPermissionsError\n\ntry:\n    connector.validate_connector_settings()\nexcept InsufficientPermissionsError:\n    surface_to_admin(\n        f\"Grant token user at least Reporter on {connector.project_owner}/{connector.project_name} \"\n        f\"and give the token the 'api' scope.\"\n    )","preventionTips":["Document required role (Reporter+) and scopes (api) next to the credential form","Use a dedicated bot/service account with explicit project membership instead of a personal token","For self-hosted GitLab behind IP allowlists, pin the indexer's egress IP"],"tags":["gitlab","authorization","permissions","connector","http-403"],"backgroundTag":null,"analyzedSha":"554fb1133ac3861732235ad9c377eb5e0a770665","analyzedAt":"2026-08-15T09:20:16.380Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}