From 2f68164254bcdb6874b299a14dc2aa8cf7e9dfcf Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 12:46:49 +0530 Subject: [PATCH 01/13] fix(apple): close Push without blocking the event loop and abandon an in-flight connect --- .../apple/Sources/Services/Push.swift.twig | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/templates/apple/Sources/Services/Push.swift.twig b/templates/apple/Sources/Services/Push.swift.twig index d177d5ad9e..c9f3617f12 100644 --- a/templates/apple/Sources/Services/Push.swift.twig +++ b/templates/apple/Sources/Services/Push.swift.twig @@ -104,6 +104,8 @@ open class Push: Service { private var connectionEpoch = 0 private var resubscribeOnConnect = false private var retired = Set() + private var closedClients = Set() + private var closeGeneration = 0 private var onOpenCb: (() -> Void)? private var onCloseCb: (() -> Void)? @@ -252,10 +254,14 @@ open class Push: Service { requestNotificationAuthorization() } + let generation = sync.sync { closeGeneration } let mqtt: MQTTClient do { mqtt = try await connect() } catch is SupersededError { + if sync.sync(execute: { closeGeneration != generation }) { + throw Push.closedError() + } return try await subscribe(topics.map { $0 as TopicValue }, background: background, title: title, retry: retry, callback: callback) } let epoch = sync.sync { connectionEpoch } @@ -281,6 +287,9 @@ open class Push: Service { return true } if superseded { + if sync.sync(execute: { closeGeneration != generation }) { + throw Push.closedError() + } return try await subscribe(topics.map { $0 as TopicValue }, background: background, title: title, retry: retry, callback: callback) } // The SUBSCRIBE failed; roll this call's registrations back (and drop any broker @@ -388,32 +397,49 @@ open class Push: Service { } /// Tear down the connection and drop all active subscriptions. + /// + /// Returns at once: the DISCONNECT and the client shutdown run on a background queue, + /// because a blocking wait is not allowed on the event loop that runs message callbacks, + /// and those callbacks may call `close()` or drop the last subscription. A connection + /// still being opened is abandoned, and any `subscribe` waiting on it throws. public func close() { let client = sync.sync { () -> MQTTClient? in // Mark this a deliberate close so the close listener does not reconnect. closing = true + connectionEpoch += 1 + closeGeneration += 1 let existing = mqtt mqtt = nil connecting?.cancel() connecting = nil + resubscribeOnConnect = false subscriptions.removeAll() + if let existing { + closedClients.insert(ObjectIdentifier(existing)) + } return existing } guard let client else { return } - do { - try client.disconnect().wait() - } catch MQTTError.noConnection { - // Already disconnected (e.g. the broker dropped the connection); nothing to send. - } catch { - surface(Push.describe(error)) - } - do { - try client.syncShutdownGracefully() - } catch { - surface(error) + DispatchQueue.global().async { [weak self] in + do { + try client.disconnect().wait() + } catch MQTTError.noConnection { + // Already disconnected (e.g. the broker dropped the connection); nothing to send. + } catch { + self?.surface(Push.describe(error)) + } + do { + try client.syncShutdownGracefully() + } catch { + self?.surface(error) + } } } + private static func closedError() -> Swift.Error { + {{ spec.info.title | caseUcfirst }}Error(message: "Push was closed before the subscription was established") + } + private func requestNotificationAuthorization() { #if canImport(UserNotifications) UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] _, error in @@ -605,6 +631,10 @@ open class Push: Service { if let self, let mqtt, self.sync.sync(execute: { self.retired.remove(ObjectIdentifier(mqtt)) != nil }) { return } + if let self, let mqtt, self.sync.sync(execute: { self.closedClients.remove(ObjectIdentifier(mqtt)) != nil }) { + self.onCloseCb?() + return + } if case .failure(let error) = result { self?.surface(Push.describe(error)) } else if let self, !self.sync.sync(execute: { self.closing }) { From 5081c0a6ca5ca69ec9224fcf283c3c5e76585c44 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 12:46:49 +0530 Subject: [PATCH 02/13] fix(react-native): stop a subscribe waiting on a connection when Push is closed --- templates/react-native/src/services/push.ts.twig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/templates/react-native/src/services/push.ts.twig b/templates/react-native/src/services/push.ts.twig index 942a65f97c..2d31304b57 100644 --- a/templates/react-native/src/services/push.ts.twig +++ b/templates/react-native/src/services/push.ts.twig @@ -120,6 +120,8 @@ export class Push extends Service { private nativeActive = false; /** Bumped when the subscriptions move to the native host, superseding an open() in flight. */ private connectionEpoch = 0; + /** Bumped by close(), so a subscribe that was waiting on a connection stops instead of reopening it. */ + private closeGeneration = 0; private nativeEvents: { remove(): void }[] | null = null; private onOpenCb?: () => void; @@ -378,6 +380,7 @@ export class Push extends Service { callbackOrOptions: MessageCallback | SubscribeOptions | undefined, topicOptions: SubscribeOptions, ): Promise { + const generation = this.closeGeneration; await this.loadCookieSession(); // Topic-less form: subscribe(callback, options) targets the signed-in user's topic. const userForm = typeof topicsOrCallback === 'function'; @@ -460,6 +463,9 @@ export class Push extends Service { // Another Push moved this one's subscriptions to the native host meanwhile: this // subscribe goes there too. if (err instanceof SupersededError) { + if (generation !== this.closeGeneration) { + throw closedError(); + } return this.subscribeWith( topicsOrCallback, callbackOrOptions, @@ -516,6 +522,9 @@ export class Push extends Service { for (const id of ids) { this.subscriptions.delete(id); } + if (generation !== this.closeGeneration) { + throw closedError(); + } return this.subscribeWith( topicsOrCallback, callbackOrOptions, @@ -892,6 +901,8 @@ export class Push extends Service { * their subscriptions and the saved ones. */ close(): void { + this.connectionEpoch++; + this.closeGeneration++; this.teardown(); const native = this.native; if (!native) { @@ -1391,6 +1402,10 @@ const BACKGROUND_DISPLACED = 'and the device hosts one. These subscriptions still deliver while the app runs.'; /** An in-process connect superseded by the native host or by a connect with another credential. */ +function closedError(): Error { + return new Error('Push was closed before the subscription was established'); +} + class SupersededError extends Error { constructor() { super('The push connection was superseded'); From cf91d73e761dce5ca9f2f2c606d752aff95dddf4 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 12:46:49 +0530 Subject: [PATCH 03/13] fix: send a file of exactly the chunk size in a single request --- .../android/library/src/main/java/io/package/Client.kt.twig | 2 +- templates/apple/Sources/Client.swift.twig | 2 +- templates/dotnet/Package/Client.cs.twig | 2 +- templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig | 2 +- templates/python/package/client.py.twig | 2 +- templates/ruby/lib/container/client.rb.twig | 2 +- templates/swift/Sources/Client.swift.twig | 2 +- templates/unity/Assets/Runtime/Core/Client.cs.twig | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index 72f6e3042b..5b31f828e7 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -476,7 +476,7 @@ class Client @JvmOverloads constructor( else -> throw UnsupportedOperationException() } - if (size < CHUNK_SIZE || responseType == String::class.java) { + if (size <= CHUNK_SIZE || responseType == String::class.java) { val data = when (input.sourceType) { "file", "path" -> File(input.path).asRequestBody() "bytes" -> (input.data as ByteArray).toRequestBody(input.mimeType.toMediaType()) diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 9fe9b996c9..26f8607ee7 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -491,7 +491,7 @@ open class Client { let size = (input.data as! ByteBuffer).readableBytes - if size < Client.chunkSize || T.self == String.self { + if size <= Client.chunkSize || T.self == String.self { params[paramName] = input return try await call( method: method, diff --git a/templates/dotnet/Package/Client.cs.twig b/templates/dotnet/Package/Client.cs.twig index 6f1136c45b..a896d79e40 100644 --- a/templates/dotnet/Package/Client.cs.twig +++ b/templates/dotnet/Package/Client.cs.twig @@ -436,7 +436,7 @@ namespace {{ spec.info.title | caseUcfirst }} var offset = 0L; var result = new Dictionary(); - if (size < ChunkSize || typeof(T) == typeof(string)) + if (size <= ChunkSize || typeof(T) == typeof(string)) { byte[] buffer; switch (input.SourceType) diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index cdb34152db..290afaa7ca 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -402,7 +402,7 @@ class Client @JvmOverloads constructor( else -> throw UnsupportedOperationException() } - if (size < CHUNK_SIZE || responseType == String::class.java) { + if (size <= CHUNK_SIZE || responseType == String::class.java) { val data = when (input.sourceType) { "file", "path" -> File(input.path).asRequestBody() "bytes" -> (input.data as ByteArray).toRequestBody(input.mimeType.toMediaType()) diff --git a/templates/python/package/client.py.twig b/templates/python/package/client.py.twig index b70d446a64..02b9d6a3e7 100644 --- a/templates/python/package/client.py.twig +++ b/templates/python/package/client.py.twig @@ -168,7 +168,7 @@ class Client: size = len(input_file.data) input = input_file.data - if size < self._chunk_size or response_type == 'text': + if size <= self._chunk_size or response_type == 'text': if input_file.source_type == 'path': with open(input_file.path, 'rb') as input: input_file.data = input.read() diff --git a/templates/ruby/lib/container/client.rb.twig b/templates/ruby/lib/container/client.rb.twig index 74497fd28b..5d90805893 100644 --- a/templates/ruby/lib/container/client.rb.twig +++ b/templates/ruby/lib/container/client.rb.twig @@ -145,7 +145,7 @@ module {{ spec.info.title | caseUcfirst }} size = input_file.data.bytesize end - if size < @chunk_size || response_type == "text" + if size <= @chunk_size || response_type == "text" if input_file.source_type == 'path' input_file.data = IO.read(input_file.path) end diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 048c205f37..18d0982bac 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -457,7 +457,7 @@ open class Client { let size = (input.data as! ByteBuffer).readableBytes - if size < Client.chunkSize || T.self == String.self { + if size <= Client.chunkSize || T.self == String.self { params[paramName] = input return try await call( method: method, diff --git a/templates/unity/Assets/Runtime/Core/Client.cs.twig b/templates/unity/Assets/Runtime/Core/Client.cs.twig index e9e157c556..a073e25bad 100644 --- a/templates/unity/Assets/Runtime/Core/Client.cs.twig +++ b/templates/unity/Assets/Runtime/Core/Client.cs.twig @@ -801,7 +801,7 @@ namespace {{ spec.info.title | caseUcfirst }} var buffer = new byte[Math.Min(size, ChunkSize)]; var result = new Dictionary(); - if (size < ChunkSize || typeof(T) == typeof(string)) + if (size <= ChunkSize || typeof(T) == typeof(string)) { switch (input.SourceType) { From b88ba37e55616798cd762d3a1136e142a2f3b9ad Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 12:46:49 +0530 Subject: [PATCH 04/13] fix(dart): type example results with the models alias --- templates/dart/docs/example.md.twig | 5 ++++- templates/flutter/docs/example.md.twig | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/templates/dart/docs/example.md.twig b/templates/dart/docs/example.md.twig index b1587a2464..d4ce2c4793 100644 --- a/templates/dart/docs/example.md.twig +++ b/templates/dart/docs/example.md.twig @@ -3,6 +3,9 @@ import 'dart:io'; {% endif %} import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; +{% if method.method.value != 'delete' and (method | methodType) not in ['webAuth', 'location'] and (method | getValidResponseModels)|length <= 1 and (method | responseModel) %} +import 'package:{{ language.params.packageName }}/models.dart' as models; +{% endif %} {% set hasEnumParam = false %} {% for parameter in (method | parameters('all')) %} {% if (parameter | enumValues) | length > 0 %} @@ -29,7 +32,7 @@ Client client = Client() {{ service.name | caseUcfirst }} {{ service.name | caseCamel }} = {{service.name | caseUcfirst}}(client); -{% if method.method.value != 'delete' and (method | methodType) != 'webAuth' %}{% if (method | methodType) == 'location' %}Uint8List{% elseif (method | getValidResponseModels)|length > 1 %}dynamic{% else %}{{ (method | responseModel) | caseUcfirst | overrideIdentifier }}{% endif %} result = {% endif %}await {{ service.name | caseCamel }}.{{ (method | methodName) | caseCamel }}({% if (method | parameters('all')) | length == 0 %});{% endif %} +{% if method.method.value != 'delete' and (method | methodType) != 'webAuth' %}{% if (method | methodType) == 'location' %}Uint8List{% elseif (method | getValidResponseModels)|length > 1 %}dynamic{% else %}models.{{ (method | responseModel) | caseUcfirst | overrideIdentifier }}{% endif %} result = {% endif %}await {{ service.name | caseCamel }}.{{ (method | methodName) | caseCamel }}({% if (method | parameters('all')) | length == 0 %});{% endif %} {%~ for parameter in (method | parameters('all')) %} {{ parameter.name | caseCamel | overrideIdentifier }}: {% if (parameter | enumValues) | length > 0 %}{{ parameter | enumExample }}{% else %}{{ parameter | paramExample | replace({'': (parameter.name | caseCamel) }) | raw }}{% endif %},{% if not parameter.required %} // (optional){% endif %} diff --git a/templates/flutter/docs/example.md.twig b/templates/flutter/docs/example.md.twig index 8e62609b6b..573f6efec5 100644 --- a/templates/flutter/docs/example.md.twig +++ b/templates/flutter/docs/example.md.twig @@ -7,6 +7,9 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; {% endif %} import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; +{% if method.method.value != 'delete' and (method | methodType) not in ['webAuth', 'location'] and (method | getValidResponseModels)|length <= 1 and (method | responseModel) %} +import 'package:{{ language.params.packageName }}/models.dart' as models; +{% endif %} {% set hasEnumParam = false %} {% for parameter in (method | parameters('all')) %} {% if (parameter | enumValues) | length > 0 %} @@ -59,7 +62,7 @@ FutureBuilder( }, ); {% else %} -{% if method.method.value != 'delete' and (method | methodType) != 'webAuth' %}{% if (method | methodType) == 'location' %}Uint8List{% elseif (method | getValidResponseModels)|length > 1 %}dynamic{% else %}{{ (method | responseModel) | caseUcfirst | overrideIdentifier }}{% endif %} result = {% endif %}await {{ service.name | caseCamel }}.{{ (method | methodName) | caseCamel }}({% if (method | parameters('all')) | length == 0 %});{% endif %} +{% if method.method.value != 'delete' and (method | methodType) != 'webAuth' %}{% if (method | methodType) == 'location' %}Uint8List{% elseif (method | getValidResponseModels)|length > 1 %}dynamic{% else %}models.{{ (method | responseModel) | caseUcfirst | overrideIdentifier }}{% endif %} result = {% endif %}await {{ service.name | caseCamel }}.{{ (method | methodName) | caseCamel }}({% if (method | parameters('all')) | length == 0 %});{% endif %} {%~ for parameter in (method | parameters('all')) %} {{ parameter.name | caseCamel | overrideIdentifier}}: {% if (parameter | enumValues) | length > 0%}{{ parameter | enumExample }}{% else %}{{ parameter | paramExample | replace({'': (parameter.name | caseCamel) }) | raw }}{% endif %},{% if not parameter.required %} // optional{% endif %} From 31418186e3a0d38499ab8b3ecf1855a78974138d Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 12:46:49 +0530 Subject: [PATCH 05/13] fix(dart): escape $ in example values so keys like $id are not interpolated --- src/SDK/Language/Dart.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/SDK/Language/Dart.php b/src/SDK/Language/Dart.php index 46a35d5919..a751e0d981 100644 --- a/src/SDK/Language/Dart.php +++ b/src/SDK/Language/Dart.php @@ -211,18 +211,23 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str } return match ($type) { - self::TYPE_ARRAY => $this->isPermissionString($example) ? $this->getPermissionExample($example) : $example, + self::TYPE_ARRAY => $this->isPermissionString($example) ? $this->getPermissionExample($example) : $this->escapeInterpolation((string) $example), self::TYPE_FILE, self::TYPE_INTEGER, self::TYPE_NUMBER => $example, self::TYPE_BOOLEAN => ($example) ? 'true' : 'false', self::TYPE_OBJECT => ($decoded = json_decode((string) $example, true)) !== null ? (empty($decoded) && $example === '{}' ? '{}' - : preg_replace('/\n/', "\n ", json_encode($decoded, JSON_PRETTY_PRINT))) - : $example, - self::TYPE_STRING => "'{$example}'", + : $this->escapeInterpolation((string) preg_replace('/\n/', "\n ", json_encode($decoded, JSON_PRETTY_PRINT)))) + : $this->escapeInterpolation((string) $example), + self::TYPE_STRING => "'" . $this->escapeInterpolation((string) $example) . "'", }; } + private function escapeInterpolation(string $value): string + { + return str_replace('$', '\$', str_replace('\$', '$', $value)); + } + public function getModelToMapValue(Schema $property, string $propertyName, bool $required): string { $name = $this->escapeKeyword($propertyName); From 84632c58ba136ebb727ad7bee94cbcbb946bee40 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 12:46:49 +0530 Subject: [PATCH 06/13] fix(node): import InputFile in upload examples --- templates/node/docs/example.md.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/node/docs/example.md.twig b/templates/node/docs/example.md.twig index ab3670760d..1a740a58f2 100644 --- a/templates/node/docs/example.md.twig +++ b/templates/node/docs/example.md.twig @@ -1,7 +1,7 @@ ```javascript const sdk = require('node-{{ spec.info.title | caseDash }}'); {% if (method | consumes)[0] == 'multipart/form-data' %} -const fs = require('fs'); +const { InputFile } = require('node-{{ spec.info.title | caseDash }}/file'); {% endif %} {% set clientCalls = [] %}{% if [(method | securitySchemes)]|length > 0 %}{% set clientCalls = clientCalls|merge([{'call': 'setEndpoint', 'argument': endpointDocs, 'comment': 'Your API Endpoint'}]) %}{% for node in [(method | securitySchemes)] %}{% for key,header in node|keys %}{% set clientCalls = clientCalls|merge([{'call': ('set' ~ header), 'argument': node[header].extensions['x-appwrite']['demo'], 'comment': node[header].description}]) %}{% endfor %}{% endfor %}{% endif %} From e8b6225f7562ab644c6c9b47f575e7bf43659804 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 12:46:49 +0530 Subject: [PATCH 07/13] test(node): cover single-request, chunked, text and file-less uploads --- templates/node/test/client.test.js.twig | 110 ++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/templates/node/test/client.test.js.twig b/templates/node/test/client.test.js.twig index c13af6c448..a8bec62234 100644 --- a/templates/node/test/client.test.js.twig +++ b/templates/node/test/client.test.js.twig @@ -1,4 +1,5 @@ const { Client } = require('../dist/client'); +const { InputFile } = require('../dist/inputFile'); const { fetch: mockedFetch, Response, Dispatcher } = require('undici'); jest.mock('undici', () => ({ @@ -43,3 +44,112 @@ describe('Client', () => { } }); }); + +describe('Client.chunkedUpload', () => { + const url = new URL( + 'https://cloud.appwrite.io/v1/storage/buckets/bucket/files', + ); + const headers = { 'content-type': 'multipart/form-data' }; + const sentHeaders = (index) => mockedFetch.mock.calls[index][1].headers; + const file = (size) => + InputFile.fromBuffer(Buffer.alloc(size), 'upload.bin'); + + beforeEach(() => { + mockedFetch.mockReset(); + }); + + test('sends a file of exactly the chunk size in one request', async () => { + mockedFetch.mockImplementation(() => Response.json({ $id: 'file' })); + const client = new Client(); + + await client.chunkedUpload( + 'POST', + url, + { ...headers }, + { file: file(Client.CHUNK_SIZE) }, + () => {}, + ); + + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(sentHeaders(0)).not.toHaveProperty('content-range'); + }); + + test('splits a larger file into chunks that share the upload id', async () => { + mockedFetch.mockImplementation(() => + Response.json({ + $id: 'file', + chunksUploaded: mockedFetch.mock.calls.length, + chunksTotal: 2, + }), + ); + const client = new Client(); + const size = Client.CHUNK_SIZE + 1; + + const result = await client.chunkedUpload( + 'POST', + url, + { ...headers }, + { file: file(size) }, + () => {}, + ); + + expect(mockedFetch).toHaveBeenCalledTimes(2); + expect(sentHeaders(0)['content-range']).toBe( + `bytes 0-${Client.CHUNK_SIZE - 1}/${size}`, + ); + expect(sentHeaders(1)['content-range']).toBe( + `bytes ${Client.CHUNK_SIZE}-${Client.CHUNK_SIZE}/${size}`, + ); + expect(sentHeaders(1)['x-{{ spec.info.title | caseLower }}-id']).toBe('file'); + expect(result).toEqual({ + $id: 'file', + chunksUploaded: 2, + chunksTotal: 2, + }); + }); + + test('sends a text upload in one request whatever its size', async () => { + mockedFetch.mockImplementation( + () => + new Response('zone file', { + headers: { 'content-type': 'text/plain' }, + }), + ); + const client = new Client(); + + const result = await client.chunkedUpload( + 'POST', + url, + { ...headers }, + { file: file(Client.CHUNK_SIZE * 2 + 1) }, + () => {}, + 'text', + ); + + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(sentHeaders(0)).not.toHaveProperty('content-range'); + expect(result).toBe('zone file'); + }); + + test('makes a plain call with the requested response type when no file is sent', async () => { + mockedFetch.mockImplementation( + () => + new Response('plain text', { + headers: { 'content-type': 'text/plain' }, + }), + ); + const client = new Client(); + + const result = await client.chunkedUpload( + 'POST', + url, + { ...headers }, + { name: 'zone' }, + () => {}, + 'text', + ); + + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(result).toBe('plain text'); + }); +}); From 961fb3658149c6df560031473e27b6a427a42d6e Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 12:46:49 +0530 Subject: [PATCH 08/13] docs(flutter): document the minSdk 24 requirement and the Android 13 notification permission --- templates/flutter/README.md.twig | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/templates/flutter/README.md.twig b/templates/flutter/README.md.twig index 64b8eb9277..07cd245410 100644 --- a/templates/flutter/README.md.twig +++ b/templates/flutter/README.md.twig @@ -38,12 +38,16 @@ flutter pub add {{ language.params.packageName }} ### Push on Android -`Push` includes a native Android plugin, and `flutter_local_notifications` needs core library -desugaring, so add this to `android/app/build.gradle.kts` (desugaring is configured when the app -is assembled, so a plugin cannot apply it for you): +`Push` includes a native Android plugin, so the SDK needs Android 7.0 (API 24) or later: apps +with a lower `minSdk` fail to build until they raise it. `flutter_local_notifications` also needs +core library desugaring. Add both to `android/app/build.gradle.kts` (desugaring is configured when +the app is assembled, so a plugin cannot apply it for you): ```kotlin android { + defaultConfig { + minSdk = 24 + } compileOptions { isCoreLibraryDesugaringEnabled = true } @@ -61,7 +65,16 @@ seconds to reconnect; the broker replays what was sent in between (`retry: true` in-app callback receives are posted as notifications that open the app. It reconnects with the credential saved at subscribe time, so use a session rather than a short-lived JWT. +On Android 13 and later, notifications only appear once the user grants the +`POST_NOTIFICATIONS` runtime permission. Without it, the subscription still delivers to your +callback but posts no notification, so ask before subscribing, for example with +`flutter_local_notifications`: + ```dart +await FlutterLocalNotificationsPlugin() + .resolvePlatformSpecificImplementation() + ?.requestNotificationsPermission(); + final sub = await push.subscribe('news', (message) => print(message.data), background: true, title: 'News'); From 1734e72554bf49dd53d077a418e74cafd7c14043 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 12:46:49 +0530 Subject: [PATCH 09/13] docs(react-native): request the Android 13 notification permission before background delivery --- templates/react-native/README.md.twig | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/templates/react-native/README.md.twig b/templates/react-native/README.md.twig index ade3d3be34..403a852389 100644 --- a/templates/react-native/README.md.twig +++ b/templates/react-native/README.md.twig @@ -50,7 +50,17 @@ between (`retry: true`). Messages no in-app callback receives are posted as noti open the app. It reconnects with the credential saved at subscribe time, so use a session rather than a short-lived JWT. +On Android 13 and later, notifications only appear once the user grants the +`POST_NOTIFICATIONS` runtime permission (the SDK declares it in its manifest). Without it, the +subscription still delivers to your callback but posts no notification, so ask before subscribing: + ```js +import { PermissionsAndroid, Platform } from 'react-native'; + +if (Platform.OS === 'android' && Platform.Version >= 33) { + await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS); +} + const sub = await push.subscribe('news', (message) => console.log(message.data), { background: true, title: 'News', From b657b7b8903edf1a86840836950514d6fb5a4dde Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 12:46:49 +0530 Subject: [PATCH 10/13] test: close Push while connecting and from a message callback --- tests/e2e/AppleSwift61Test.php | 4 +- tests/e2e/Base.php | 8 +++ tests/e2e/ReactNativeAndroidTest.php | 1 + tests/e2e/languages/apple/Tests.swift | 51 +++++++++++++++++++ tests/e2e/languages/react-native/push.node.js | 16 ++++++ 5 files changed, 79 insertions(+), 1 deletion(-) diff --git a/tests/e2e/AppleSwift61Test.php b/tests/e2e/AppleSwift61Test.php index a45a41b021..1a5b0b0094 100644 --- a/tests/e2e/AppleSwift61Test.php +++ b/tests/e2e/AppleSwift61Test.php @@ -60,6 +60,8 @@ final class AppleSwift61Test extends Base ...Base::OPERATOR_HELPER_RESPONSES, ...Base::PUSH_RESPONSES, ...Base::PUSH_ERROR_RESPONSES, - ...Base::PUSH_CREDENTIAL_SWITCH_RESPONSES + ...Base::PUSH_CREDENTIAL_SWITCH_RESPONSES, + ...Base::PUSH_CLOSE_WHILE_CONNECTING_RESPONSES, + ...Base::PUSH_CLOSE_FROM_CALLBACK_RESPONSES, ]; } diff --git a/tests/e2e/Base.php b/tests/e2e/Base.php index 6908bf7678..fe4bc91396 100644 --- a/tests/e2e/Base.php +++ b/tests/e2e/Base.php @@ -206,6 +206,14 @@ abstract class Base extends TestCase 'Push credential pending switch:passed', ]; + protected const PUSH_CLOSE_WHILE_CONNECTING_RESPONSES = [ + 'Push close while connecting:passed', + ]; + + protected const PUSH_CLOSE_FROM_CALLBACK_RESPONSES = [ + 'Push close from callback:passed', + ]; + protected const WEB_PUSH_CONNECTION_RESPONSES = [ 'Push concurrent refused:passed', 'Push tab client id:passed', diff --git a/tests/e2e/ReactNativeAndroidTest.php b/tests/e2e/ReactNativeAndroidTest.php index 67fd18c002..b6367c598e 100644 --- a/tests/e2e/ReactNativeAndroidTest.php +++ b/tests/e2e/ReactNativeAndroidTest.php @@ -70,6 +70,7 @@ final class ReactNativeAndroidTest extends Base ...Base::PUSH_SESSION_REPLAY_RESPONSES, ...Base::PUSH_ERROR_RESPONSES, ...Base::PUSH_NATIVE_DISPLACED_RESPONSES, + ...Base::PUSH_CLOSE_WHILE_CONNECTING_RESPONSES, ...Base::PUSH_BACKGROUND_RESPONSES, ...Base::PUSH_NATIVE_SESSION_COOKIE_RESPONSES, ]; diff --git a/tests/e2e/languages/apple/Tests.swift b/tests/e2e/languages/apple/Tests.swift index 893c4c9a44..864e25ed54 100644 --- a/tests/e2e/languages/apple/Tests.swift +++ b/tests/e2e/languages/apple/Tests.swift @@ -692,6 +692,53 @@ class Tests: XCTestCase { ? "Push credential pending switch:passed" : "Push credential pending switch:failed (first: \(firstPendingOutcome), second: \(secondPending))" ) + + let closingPush = Push( + Client() + .setProject("console") + .setSelfSigned() + .setPushEndpoint("mqtt://mqtt:1883") + .setJWT("slow:closing") + ) + let closedWhileConnecting = ErrorCollector() + Task { + closedWhileConnecting.record(await outcome { _ = try await closingPush.subscribe("e2e-switch") { _ in } }) + } + try? await Task.sleep(nanoseconds: 100_000_000) + closingPush.close() + for _ in 0..<100 where closedWhileConnecting.message == nil { + try? await Task.sleep(nanoseconds: 100_000_000) + } + let closedOutcome = closedWhileConnecting.message ?? "timeout" + print( + closedOutcome == "Push was closed before the subscription was established" + ? "Push close while connecting:passed" + : "Push close while connecting:failed (\(closedOutcome))" + ) + + let callbackPush = Push( + Client() + .setProject("console") + .setSelfSigned() + .setPushEndpoint("mqtt://mqtt:1883") + .setSession(e2eSession) + ) + let handle = SubscriptionBox() + let closedFromCallback = ErrorCollector() + handle.subscription = try? await callbackPush.subscribe("e2e-push") { _ in + handle.subscription?.unsubscribe() + closedFromCallback.record("closed") + } + for _ in 0..<50 where closedFromCallback.message == nil { + try? await Task.sleep(nanoseconds: 100_000_000) + } + let reopened = await outcome { _ = try await callbackPush.subscribe("e2e-switch") { _ in } } + callbackPush.close() + print( + closedFromCallback.message == "closed" && reopened.isEmpty + ? "Push close from callback:passed" + : "Push close from callback:failed (callback: \(closedFromCallback.message ?? "none"), reopened: \(reopened))" + ) } func parse(from json: String) -> String? { @@ -722,6 +769,10 @@ final class TopicCollector: @unchecked Sendable { } } +final class SubscriptionBox: @unchecked Sendable { + var subscription: PushSubscription? +} + final class ErrorCollector: @unchecked Sendable { private let lock = NSLock() private var first: String? diff --git a/tests/e2e/languages/react-native/push.node.js b/tests/e2e/languages/react-native/push.node.js index 9d93e1886c..43f4e4e170 100644 --- a/tests/e2e/languages/react-native/push.node.js +++ b/tests/e2e/languages/react-native/push.node.js @@ -209,6 +209,22 @@ async function main() { secondUser.close(); delete NativeModules.AppwritePush; + const closingPush = new Push( + new Client().setProject('console').setPushEndpoint(ENDPOINT).setJWT('slow:closing'), + ); + const closingOutcome = closingPush.subscribe(['e2e-switch'], () => {}).then( + () => '', + (e) => (e instanceof Error ? e.message : String(e)), + ); + await timeout(100); + closingPush.close(); + const closedOutcome = await Promise.race([closingOutcome, timeout(5000, 'timeout')]); + console.log( + closedOutcome === 'Push was closed before the subscription was established' + ? 'Push close while connecting:passed' + : `Push close while connecting:failed (${closedOutcome})`, + ); + // mqtt.js keepalive timers would otherwise hold the event loop open. process.exit(0); } From acdab474f8079cd8ef74cf6b02a995376925af2f Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 13:10:27 +0530 Subject: [PATCH 11/13] fix(react-native): check for close() after every await before a subscribe registers or connects --- .../react-native/src/services/push.ts.twig | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/templates/react-native/src/services/push.ts.twig b/templates/react-native/src/services/push.ts.twig index 2d31304b57..1075b51a6a 100644 --- a/templates/react-native/src/services/push.ts.twig +++ b/templates/react-native/src/services/push.ts.twig @@ -382,6 +382,7 @@ export class Push extends Service { ): Promise { const generation = this.closeGeneration; await this.loadCookieSession(); + this.throwIfClosedSince(generation); // Topic-less form: subscribe(callback, options) targets the signed-in user's topic. const userForm = typeof topicsOrCallback === 'function'; const topics = userForm ? this.userTopic() : topicsOrCallback; @@ -427,6 +428,7 @@ export class Push extends Service { nativeHosts.size > 0 || (await this.native.hasSaved())) ) { + this.throwIfClosedSince(generation); const ids = topicList.map((topic) => { const id = randomId(); this.subscriptions.set(id, { @@ -455,6 +457,7 @@ export class Push extends Service { if (background) { await this.startBackground(); } + this.throwIfClosedSince(generation); let client: MqttClient; try { @@ -463,9 +466,7 @@ export class Push extends Service { // Another Push moved this one's subscriptions to the native host meanwhile: this // subscribe goes there too. if (err instanceof SupersededError) { - if (generation !== this.closeGeneration) { - throw closedError(); - } + this.throwIfClosedSince(generation); return this.subscribeWith( topicsOrCallback, callbackOrOptions, @@ -522,9 +523,7 @@ export class Push extends Service { for (const id of ids) { this.subscriptions.delete(id); } - if (generation !== this.closeGeneration) { - throw closedError(); - } + this.throwIfClosedSince(generation); return this.subscribeWith( topicsOrCallback, callbackOrOptions, @@ -933,6 +932,13 @@ export class Push extends Service { this.stopBackground(); } + /** Throws once close() has run since `generation` was read, so a subscribe started before it stops. */ + private throwIfClosedSince(generation: number): void { + if (generation !== this.closeGeneration) { + throw closedError(); + } + } + /** True while any live subscription has opted into background delivery. */ private hasBackgroundSubs(): boolean { for (const sub of this.subscriptions.values()) { From e1e970546205b8ffd5239abcf02b75f794d5439c Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 13:10:28 +0530 Subject: [PATCH 12/13] test(react-native): close Push right after calling subscribe --- tests/e2e/Base.php | 4 ++++ tests/e2e/ReactNativeAndroidTest.php | 1 + tests/e2e/languages/react-native/push.node.js | 17 +++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/tests/e2e/Base.php b/tests/e2e/Base.php index fe4bc91396..878e9742a1 100644 --- a/tests/e2e/Base.php +++ b/tests/e2e/Base.php @@ -210,6 +210,10 @@ abstract class Base extends TestCase 'Push close while connecting:passed', ]; + protected const PUSH_CLOSE_RIGHT_AFTER_SUBSCRIBE_RESPONSES = [ + 'Push close right after subscribe:passed', + ]; + protected const PUSH_CLOSE_FROM_CALLBACK_RESPONSES = [ 'Push close from callback:passed', ]; diff --git a/tests/e2e/ReactNativeAndroidTest.php b/tests/e2e/ReactNativeAndroidTest.php index b6367c598e..22ea186129 100644 --- a/tests/e2e/ReactNativeAndroidTest.php +++ b/tests/e2e/ReactNativeAndroidTest.php @@ -71,6 +71,7 @@ final class ReactNativeAndroidTest extends Base ...Base::PUSH_ERROR_RESPONSES, ...Base::PUSH_NATIVE_DISPLACED_RESPONSES, ...Base::PUSH_CLOSE_WHILE_CONNECTING_RESPONSES, + ...Base::PUSH_CLOSE_RIGHT_AFTER_SUBSCRIBE_RESPONSES, ...Base::PUSH_BACKGROUND_RESPONSES, ...Base::PUSH_NATIVE_SESSION_COOKIE_RESPONSES, ]; diff --git a/tests/e2e/languages/react-native/push.node.js b/tests/e2e/languages/react-native/push.node.js index 43f4e4e170..92d4772106 100644 --- a/tests/e2e/languages/react-native/push.node.js +++ b/tests/e2e/languages/react-native/push.node.js @@ -225,6 +225,23 @@ async function main() { : `Push close while connecting:failed (${closedOutcome})`, ); + const quickPush = new Push(new Client().setProject('console').setPushEndpoint(ENDPOINT).setSession(e2eSession)); + let quickOpened = false; + quickPush.onOpen(() => (quickOpened = true)); + const quickOutcome = quickPush.subscribe(['e2e-switch'], () => {}).then( + () => '', + (e) => (e instanceof Error ? e.message : String(e)), + ); + quickPush.close(); + const quickClosed = await Promise.race([quickOutcome, timeout(5000, 'timeout')]); + await timeout(200); + console.log( + quickClosed === 'Push was closed before the subscription was established' && !quickOpened + ? 'Push close right after subscribe:passed' + : `Push close right after subscribe:failed (${quickClosed}, opened: ${quickOpened})`, + ); + quickPush.close(); + // mqtt.js keepalive timers would otherwise hold the event loop open. process.exit(0); } From 597af61964d59d8af975d050f4731ef48d066d8e Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 6 Oct 2026 13:10:28 +0530 Subject: [PATCH 13/13] test: upload a file of exactly the chunk size in the SDKs whose boundary changed --- mock-server/app/http.php | 12 ++++++++++++ tests/e2e/Android16Java17Test.php | 1 + tests/e2e/Android5Java17Test.php | 1 + tests/e2e/AppleSwift61Test.php | 1 + tests/e2e/Base.php | 4 ++++ tests/e2e/DotNet60Test.php | 1 + tests/e2e/DotNet80Test.php | 1 + tests/e2e/DotNet90Test.php | 1 + tests/e2e/KotlinJava17Test.php | 1 + tests/e2e/Python310Test.php | 1 + tests/e2e/Python311Test.php | 1 + tests/e2e/Python312Test.php | 1 + tests/e2e/Python313Test.php | 1 + tests/e2e/Python39Test.php | 1 + tests/e2e/Ruby27Test.php | 1 + tests/e2e/Ruby30Test.php | 1 + tests/e2e/Ruby31Test.php | 1 + tests/e2e/Swift61Test.php | 1 + tests/e2e/Unity2021Test.php | 1 + tests/e2e/languages/android/Tests.kt | 7 +++++++ tests/e2e/languages/apple/Tests.swift | 8 ++++++++ tests/e2e/languages/dotnet/Tests.cs | 3 +++ tests/e2e/languages/kotlin/Tests.kt | 7 +++++++ tests/e2e/languages/python/tests.py | 3 +++ tests/e2e/languages/ruby/tests.rb | 7 +++++++ tests/e2e/languages/swift/Tests.swift | 8 ++++++++ tests/e2e/languages/unity/Tests.cs | 3 +++ 27 files changed, 79 insertions(+) diff --git a/mock-server/app/http.php b/mock-server/app/http.php index 606affb57f..68c0566f77 100644 --- a/mock-server/app/http.php +++ b/mock-server/app/http.php @@ -419,6 +419,10 @@ throw new Exception(Exception::GENERAL_MOCK, 'Invalid content-range header'); } + if ($start === 0 && $end === $size - 1 && $size === $chunkSize) { + throw new Exception(Exception::GENERAL_MOCK, 'A file of exactly the chunk size must be sent in one request'); + } + if ($start === 0 && !empty($id)) { throw new Exception(Exception::GENERAL_MOCK, 'First chunked request cannot have id header'); } @@ -460,6 +464,14 @@ $file['name'] = (\is_array($file['name'])) ? $file['name'][0] : $file['name']; $file['size'] = (\is_array($file['size'])) ? $file['size'][0] : $file['size']; + if ($file['name'] === 'boundary.bin') { + if ($file['size'] !== $chunkSize) { + throw new Exception(Exception::GENERAL_MOCK, 'Wrong boundary file size'); + } + + return; + } + if ($file['name'] !== 'file.png') { throw new Exception(Exception::GENERAL_MOCK, 'Wrong file name'); } diff --git a/tests/e2e/Android16Java17Test.php b/tests/e2e/Android16Java17Test.php index 762472a4e7..e86b8144c2 100644 --- a/tests/e2e/Android16Java17Test.php +++ b/tests/e2e/Android16Java17Test.php @@ -48,6 +48,7 @@ final class Android16Java17Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/Android5Java17Test.php b/tests/e2e/Android5Java17Test.php index faa947f12f..8d2c89dc40 100644 --- a/tests/e2e/Android5Java17Test.php +++ b/tests/e2e/Android5Java17Test.php @@ -45,6 +45,7 @@ final class Android5Java17Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/AppleSwift61Test.php b/tests/e2e/AppleSwift61Test.php index 1a5b0b0094..8a8996e512 100644 --- a/tests/e2e/AppleSwift61Test.php +++ b/tests/e2e/AppleSwift61Test.php @@ -44,6 +44,7 @@ final class AppleSwift61Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/Base.php b/tests/e2e/Base.php index 878e9742a1..b283e22b8f 100644 --- a/tests/e2e/Base.php +++ b/tests/e2e/Base.php @@ -154,6 +154,10 @@ abstract class Base extends TestCase 'PUT:/v1/mock/tests/general/upload-generic:passed', ]; + protected const BOUNDARY_UPLOAD_RESPONSES = [ + 'POST:/v1/mock/tests/general/upload:passed', + ]; + protected const LARGE_FILE_RESPONSES = [ 'POST:/v1/mock/tests/general/upload:passed', ]; diff --git a/tests/e2e/DotNet60Test.php b/tests/e2e/DotNet60Test.php index 3172f485a9..98a7e34296 100644 --- a/tests/e2e/DotNet60Test.php +++ b/tests/e2e/DotNet60Test.php @@ -43,6 +43,7 @@ final class DotNet60Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/DotNet80Test.php b/tests/e2e/DotNet80Test.php index a2ddf3cd02..4348cd43ab 100644 --- a/tests/e2e/DotNet80Test.php +++ b/tests/e2e/DotNet80Test.php @@ -43,6 +43,7 @@ final class DotNet80Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/DotNet90Test.php b/tests/e2e/DotNet90Test.php index 2c204655ab..930ae49ab4 100644 --- a/tests/e2e/DotNet90Test.php +++ b/tests/e2e/DotNet90Test.php @@ -43,6 +43,7 @@ final class DotNet90Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/KotlinJava17Test.php b/tests/e2e/KotlinJava17Test.php index f08b8e4bf5..9918a97715 100644 --- a/tests/e2e/KotlinJava17Test.php +++ b/tests/e2e/KotlinJava17Test.php @@ -45,6 +45,7 @@ final class KotlinJava17Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/Python310Test.php b/tests/e2e/Python310Test.php index 3d1132b4c0..b5a28d6cbc 100644 --- a/tests/e2e/Python310Test.php +++ b/tests/e2e/Python310Test.php @@ -44,6 +44,7 @@ final class Python310Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/Python311Test.php b/tests/e2e/Python311Test.php index 8305cdb49e..58621ab22d 100644 --- a/tests/e2e/Python311Test.php +++ b/tests/e2e/Python311Test.php @@ -44,6 +44,7 @@ final class Python311Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/Python312Test.php b/tests/e2e/Python312Test.php index 123f1cf663..88ecbf702b 100644 --- a/tests/e2e/Python312Test.php +++ b/tests/e2e/Python312Test.php @@ -44,6 +44,7 @@ final class Python312Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/Python313Test.php b/tests/e2e/Python313Test.php index ee5ef1bf26..928dc1ddc1 100644 --- a/tests/e2e/Python313Test.php +++ b/tests/e2e/Python313Test.php @@ -44,6 +44,7 @@ final class Python313Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/Python39Test.php b/tests/e2e/Python39Test.php index 17c2d55c6d..3037d0025c 100644 --- a/tests/e2e/Python39Test.php +++ b/tests/e2e/Python39Test.php @@ -44,6 +44,7 @@ final class Python39Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/Ruby27Test.php b/tests/e2e/Ruby27Test.php index 6673e61514..e5c310c72e 100644 --- a/tests/e2e/Ruby27Test.php +++ b/tests/e2e/Ruby27Test.php @@ -41,6 +41,7 @@ final class Ruby27Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/Ruby30Test.php b/tests/e2e/Ruby30Test.php index 2fdee30837..64a00f4bb2 100644 --- a/tests/e2e/Ruby30Test.php +++ b/tests/e2e/Ruby30Test.php @@ -41,6 +41,7 @@ final class Ruby30Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/Ruby31Test.php b/tests/e2e/Ruby31Test.php index fc8828230c..c169fae10e 100644 --- a/tests/e2e/Ruby31Test.php +++ b/tests/e2e/Ruby31Test.php @@ -41,6 +41,7 @@ final class Ruby31Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/Swift61Test.php b/tests/e2e/Swift61Test.php index e0aa3c1b61..d39fb060eb 100644 --- a/tests/e2e/Swift61Test.php +++ b/tests/e2e/Swift61Test.php @@ -44,6 +44,7 @@ final class Swift61Test extends Base ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/Unity2021Test.php b/tests/e2e/Unity2021Test.php index b3edfe3961..c24b76f38d 100644 --- a/tests/e2e/Unity2021Test.php +++ b/tests/e2e/Unity2021Test.php @@ -99,6 +99,7 @@ public function tearDown(): void ...Base::OPTIONAL_ATTACHMENT_RESPONSES, ...Base::MULTIPART_OBJECT_RESPONSES, ...Base::UPLOAD_RESPONSES, + ...Base::BOUNDARY_UPLOAD_RESPONSES, ...Base::GENERIC_UPLOAD_RESPONSES, ...Base::DOWNLOAD_RESPONSES, ...Base::ENUM_RESPONSES, diff --git a/tests/e2e/languages/android/Tests.kt b/tests/e2e/languages/android/Tests.kt index 89e1955942..bd98dabe54 100644 --- a/tests/e2e/languages/android/Tests.kt +++ b/tests/e2e/languages/android/Tests.kt @@ -250,6 +250,13 @@ class ServiceTest { writeToFile(ex.toString()) } + try { + mock = general.upload("string", 123, listOf("string in array"), InputFile.fromBytes(ByteArray(5 * 1024 * 1024), "boundary.bin", "application/octet-stream")) + writeToFile(mock.result) + } catch (ex: Exception) { + writeToFile(ex.toString()) + } + val profile = general.uploadGeneric(InputFile.fromPath("../../../../resources/file.png")) writeToFile(profile.prefs.data["result"] as String) diff --git a/tests/e2e/languages/apple/Tests.swift b/tests/e2e/languages/apple/Tests.swift index 864e25ed54..960e1b67d5 100644 --- a/tests/e2e/languages/apple/Tests.swift +++ b/tests/e2e/languages/apple/Tests.swift @@ -210,6 +210,14 @@ class Tests: XCTestCase { print(error.localizedDescription) } + do { + let file = InputFile.fromData(Data(count: 5 * 1024 * 1024), filename: "boundary.bin", mimeType: "application/octet-stream") + mock = try await general.upload(x: "string", y: 123, z: ["string in array"], file: file, onProgress: nil) + print(mock.result) + } catch { + print(error.localizedDescription) + } + let profile = try await general.uploadGeneric(file: InputFile.fromPath("\(FileManager.default.currentDirectoryPath)/../../../resources/file.png")) print(profile.prefs.data["result"]?.value as? String ?? "") diff --git a/tests/e2e/languages/dotnet/Tests.cs b/tests/e2e/languages/dotnet/Tests.cs index 5bf8d75d6a..3e9df0201b 100644 --- a/tests/e2e/languages/dotnet/Tests.cs +++ b/tests/e2e/languages/dotnet/Tests.cs @@ -117,6 +117,9 @@ public async Task Test1() mock = await general.Upload("string", 123, new List() { "string in array" }, InputFile.FromStream(info.OpenRead(), "large_file.mp4", "video/mp4")); TestContext.WriteLine(mock.Result); + mock = await general.Upload("string", 123, new List() { "string in array" }, InputFile.FromBytes(new byte[5 * 1024 * 1024], "boundary.bin", "application/octet-stream")); + TestContext.WriteLine(mock.Result); + var profile = await general.UploadGeneric(InputFile.FromPath("../../../../../../../resources/file.png")); TestContext.WriteLine(profile.Prefs.Data["result"]); diff --git a/tests/e2e/languages/kotlin/Tests.kt b/tests/e2e/languages/kotlin/Tests.kt index 99e9728482..441328c7b8 100644 --- a/tests/e2e/languages/kotlin/Tests.kt +++ b/tests/e2e/languages/kotlin/Tests.kt @@ -153,6 +153,13 @@ class ServiceTest { writeToFile(ex.toString()) } + try { + mock = general.upload("string", 123, listOf("string in array"), InputFile.fromBytes(ByteArray(5 * 1024 * 1024), "boundary.bin", "application/octet-stream")) + writeToFile(mock.result) + } catch (ex: Exception) { + writeToFile(ex.toString()) + } + val profile = general.uploadGeneric(InputFile.fromPath("../../../resources/file.png")) writeToFile(profile.prefs.data["result"] as String) diff --git a/tests/e2e/languages/python/tests.py b/tests/e2e/languages/python/tests.py index 2ba962ef81..6668a02ff7 100644 --- a/tests/e2e/languages/python/tests.py +++ b/tests/e2e/languages/python/tests.py @@ -105,6 +105,9 @@ response = general.upload('string', 123, ['string in array'], InputFile.from_bytes(data, 'large_file.mp4','video/mp4')) print(response.result) +response = general.upload('string', 123, ['string in array'], InputFile.from_bytes(bytes(5 * 1024 * 1024), 'boundary.bin', 'application/octet-stream')) +print(response.result) + response = general.upload_generic(InputFile.from_path('./tests/resources/file.png')) print(response.prefs.data['result']) diff --git a/tests/e2e/languages/ruby/tests.rb b/tests/e2e/languages/ruby/tests.rb index 6bf6c60ce2..287371cc20 100644 --- a/tests/e2e/languages/ruby/tests.rb +++ b/tests/e2e/languages/ruby/tests.rb @@ -110,6 +110,13 @@ puts e end +begin + response = general.upload(x: 'string', y: 123, z:['string in array'], file: InputFile.from_string("\0" * (5 * 1024 * 1024), filename: 'boundary.bin', mime_type: 'application/octet-stream')) + puts response.result +rescue => e + puts e +end + begin response = general.upload_generic(file: InputFile.from_path('./tests/resources/file.png')) puts response.prefs.data['result'] diff --git a/tests/e2e/languages/swift/Tests.swift b/tests/e2e/languages/swift/Tests.swift index 61435be041..4dabaffd10 100644 --- a/tests/e2e/languages/swift/Tests.swift +++ b/tests/e2e/languages/swift/Tests.swift @@ -151,6 +151,14 @@ class Tests: XCTestCase { print(error.localizedDescription) } + do { + let file = InputFile.fromData(Data(count: 5 * 1024 * 1024), filename: "boundary.bin", mimeType: "application/octet-stream") + mock = try await general.upload(x: "string", y: 123, z: ["string in array"], file: file, onProgress: nil) + print(mock.result) + } catch { + print(error.localizedDescription) + } + let profile = try await general.uploadGeneric(file: InputFile.fromPath("\(FileManager.default.currentDirectoryPath)/../../../resources/file.png")) print(profile.prefs.data["result"]?.value as? String ?? "") diff --git a/tests/e2e/languages/unity/Tests.cs b/tests/e2e/languages/unity/Tests.cs index c751419fff..d51bd1a8a2 100644 --- a/tests/e2e/languages/unity/Tests.cs +++ b/tests/e2e/languages/unity/Tests.cs @@ -203,6 +203,9 @@ private async Task RunAsyncTest() mock = await general.Upload("string", 123, new List() { "string in array" }, InputFile.FromStream(info.OpenRead(), "large_file.mp4", "video/mp4")); LogResult(mock.Result); + mock = await general.Upload("string", 123, new List() { "string in array" }, InputFile.FromBytes(new byte[5 * 1024 * 1024], "boundary.bin", "application/octet-stream")); + LogResult(mock.Result); + var profile = await general.UploadGeneric(InputFile.FromPath("../../../resources/file.png")); LogResult(profile.Prefs.Data["result"]);