Skip to content

fix(dart): send project ID as a header on location methods - #1732

Open
ChiragAgg5k wants to merge 5 commits into
mainfrom
fix/dart-location-project-header
Open

fix(dart): send project ID as a header on location methods#1732
ChiragAgg5k wants to merge 5 commits into
mainfrom
fix/dart-location-project-header

Conversation

@ChiragAgg5k

@ChiragAgg5k ChiragAgg5k commented Aug 4, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Fixes project_id_missing (403) on location-type methods — storage.getFileDownload(), getFilePreview(), getFileView(), and all avatars.* — when authenticating with a project API key. Seven SDKs were affected.

The Project security scheme is non-global, so setProject() only populates client.config; attaching X-Appwrite-Project is the per-request template's job. The server's API key check reads that header specifically, so a request carrying a valid key but no project header is rejected regardless of scopes.

Before

Future<Uint8List> getFileDownload({required String bucketId, required String fileId, String? token}) async {
    final String apiPath = '/storage/buckets/{bucketId}/files/{fileId}/download'.replaceAll('{bucketId}', bucketId).replaceAll('{fileId}', fileId);

    final Map<String, dynamic> params = {
      if (token != null) 'token': token,
      'project': client.config['project'],
      'impersonateuserid': client.config['impersonateuserid'],
    };

    final res = await client.call(HttpMethod.get, path: apiPath, params: params, responseType: ResponseType.bytes);
    return res.data;
}

After

Future<Uint8List> getFileDownload({required String bucketId, required String fileId, String? token}) async {
    final String apiPath = '/storage/buckets/{bucketId}/files/{fileId}/download'.replaceAll('{bucketId}', bucketId).replaceAll('{fileId}', fileId);

    final Map<String, dynamic> apiParams = {
      if (token != null) 'token': token,
    };

    final Map<String, String> apiHeaders = {
      'X-Appwrite-Project': client.config['project'] ?? '',
      'accept': '*/*',
    };

    final res = await client.call(HttpMethod.get, path: apiPath, params: apiParams, headers: apiHeaders, responseType: ResponseType.bytes);
    return res.data;
}

Side benefit: these methods were also missing the accept: */* header.

Affected SDKs

SDK Defect
Dart, Flutter Auth pushed into the query string via the legacy method.auth loop, no headers passed
Deno Only multipart methods sent the header — every other method, location or not, omitted it
Kotlin, Swift apiHeaders was built but never passed to the location call
Android, Apple Query string via method.auth, no headers. Apple's headers map also had to move above the branch so the shared location template can see it

Go, .NET, Node, PHP, Python, Ruby and React Native were already correct. Rust and Unity attach the header at client level in set_project/SetProject, so they were never affected.

webAuth keeps project in the query string everywhere — those are browser redirect URLs where headers aren't possible.

The dropped impersonateuserid line is not a regression. ImpersonateUserId is a global scheme, so its setter already calls addHeader(...) and the client merges it into every request — which is why securityHeaders deliberately excludes globals. The old line was dead anyway: it read config['impersonateuserid'] (from header|caseLower) while the setter writes config['impersonateUserId'], so it always resolved to null and was dropped from GET params. project only worked there because it is spelled identically in both casings.

Test Plan

The bug shipped because nothing asserted it: the mock server accepted the download request without any project header, and most SDKs never exercised a location method. Only Go, Rust and Unity asserted DOWNLOAD_RESPONSES.

  • The mock server's download route now requires a non-empty x-appwrite-project, mirroring /v1/ping. This makes the bug detectable for every language target.
  • Every SDK's e2e script calls general.download(), with DOWNLOAD_RESPONSES added to the matching expectation lists.
  • go, php, python, ruby, deno and .NET never set a project on their client, so the header was never exercised anywhere. They now do.
  • Verified passing locally: Dart, Node, PHP, Python, Ruby, Deno, Kotlin, Swift, Go, Rust, .NET.
  • The assertion has teeth in both directions. Reverting the Dart template makes its suite fail with Failed asserting that null matches expected 'GET:/v1/mock/tests/general/download:passed', and .NET fails without its client-side SetProject and passes with it.
  • The remaining suites — Apple, Android, Unity, React Native, Flutter, Web, CLI — rely on CI.
  • composer lint, composer refactor:check and the Unit suite pass. djlint reports the same 44 pre-existing errors before and after, none in the touched templates.

Two deliberate exclusions: Web, whose location methods return a URL string and issue no request, so there is no header to assert; and CLI, whose location commands write to a --destination file in a separate harness and which inherits Node's already-correct client.

Related PRs and Issues

Reported on a self-hosted 1.9.5 instance calling getFileDownload from a Dart 3.12 function with dart_appwrite 26.1.0. Workaround until the next SDK release:

final client = Client()
  ..setEndpoint(endpoint)
  ..setProject(projectId)
  ..setKey(key)
  ..addHeader('x-appwrite-project', projectId);

Separate finding, not addressed here: Ruby's location methods return a raw Net::HTTPOK object rather than bytes, unlike every other SDK. The e2e script works around it with .body.

Have you read the Contributing Guidelines on issues?

Yes.

Location methods (getFileDownload, getFilePreview, getFileView, avatars.*)
built their auth from the legacy `method.auth` loop, which put project into
the query string and sent no headers. The Project security scheme is
non-global, so `setProject()` only populates `client.config['project']` — the
`X-Appwrite-Project` header is attached per request by the other request
templates. Location methods therefore reached the server with an API key but
no project header, and the server's API key check reads that header
specifically, failing with `project_id_missing (403)`.

Build headers from `method.securityHeaders` + `method.headers`, matching
api.twig. Also fixes the missing `accept: */*` header on these methods.
OAuth/webAuth methods keep project in the query string — those are browser
redirect URLs where headers aren't possible.
@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR updates generated location requests to send project authentication through X-Appwrite-Project and expands end-to-end coverage to require that header.

  • Adds location-request security headers across the affected Dart, Flutter, Deno, Kotlin, Android, Swift, and Apple templates.
  • Makes the mock download endpoint reject requests without a project header.
  • Exercises downloads and asserts their responses across supported SDK test suites.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
templates/dart/base/requests/location.twig Moves project authentication from request parameters into typed per-request headers for Dart location methods.
templates/flutter/base/requests/location.twig Updates Flutter location requests to pass generated security and content-negotiation headers.
templates/deno/src/services/service.ts.twig Adds non-global security headers to Deno location requests.
templates/android/library/src/main/java/io/package/services/Service.kt.twig Stops serializing location authentication into query parameters and passes a dedicated header map instead.
templates/apple/Sources/Services/Service.swift.twig Moves header-map construction into shared method scope so Apple location requests can use it.
templates/swift/base/params.twig Keeps query-string authentication limited to web-auth methods while location authentication moves to headers.
templates/swift/base/requests/location.twig Passes the generated API headers into Swift location calls.
mock-server/app/http.php Strengthens the download fixture by requiring a non-empty project header before returning the expected payload.
tests/e2e/languages/deno/tests.ts Configures a project and adds a download call in the expected upload-to-enum output sequence.
tests/e2e/languages/dart/tests.dart Adds a binary download invocation and UTF-8 output assertion path to the Dart driver.

Reviews (4): Last reviewed commit: "test: set a project on the .NET e2e clie..." | Re-trigger Greptile

The bug shipped because nothing asserted it. Two layers:

Unit (tests/unit/LocationMethodTest.php) renders the Dart and Flutter SDKs
from the fixture spec and asserts general.download() carries
X-Appwrite-Project as a header and not as a query parameter. Runs in the
Unit suite, no Docker, and fails against the pre-fix templates.

E2E closes the blind spot that let this through: the mock server accepted
the download request without any project header, and neither Dart nor
Flutter exercised a location method at all. The download route now
requires the header, mirroring /v1/ping, and both scripts call
general.download().

Go never set a project on its client, so it only passed the new
assertion by accident of not being checked before — it now sets one like
every other language.
The e2e coverage added alongside it exercises the same path against a real
request, so asserting on the rendered template text was redundant.
Extending the e2e download assertion to all SDKs surfaced the same bug in
five more targets. Location methods must send X-Appwrite-Project as a
header: the Project security scheme is non-global, so setProject() only
populates client config, and the server pairs an API key against that
header specifically.

- deno: only multipart methods sent the header — every other method,
  location or not, omitted it. Now built from securityHeaders like the
  upload branch already did.
- kotlin, swift: apiHeaders was built but never passed to client.call()
  for location methods.
- android, apple: location methods pushed project into the query string
  via the legacy method.auth loop and passed no headers. Apple's headers
  map also had to move above the branch so the shared location template
  can see it.

webAuth keeps project in the query string everywhere — those are browser
redirect URLs where headers aren't possible.

Every SDK's e2e script now calls general.download(), and the mock server
requires the header on that route. php, python, ruby and deno never set a
project on their client, so they now do. Web is excluded: its location
methods return a URL string and issue no request. CLI is excluded: its
location commands write to a --destination file in a separate harness, and
it inherits Node's already-correct client.
The .NET SDK sends X-Appwrite-Project on location methods correctly, but
its e2e script never set a project, so the header went out empty and the
mock server's new assertion rejected the download. Same gap already fixed
for go, php, python, ruby and deno.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant