Describe the bug
SharePointService.CreateFile() still saves corrupted content (raw base64 ASCII text) to SharePoint when uploading binary files (images, PDFs, etc.) after upgrading to @microsoft/power-apps 1.1.3, which was supposed to resolve this in issue #306 / #230.
Steps to Reproduce
Install @microsoft/power-apps@1.1.3
Call SharePointService.CreateFile(dataset, folderPath, 'photo.jpg', base64String) where base64String is a valid base64-encoded JPEG
Open the uploaded file in SharePoint — it is not a valid image; it contains raw base64 ASCII text
Additional context
The 1.1.3 fix was incomplete. It corrected only one of two problems:
Problem 1 — Fixed in 1.1.3 ✅
In _buildOperationBodyParam, the body value was previously always passed through JSON.stringify(), which double-encoded a base64 string into ""iVBOR..."". The 1.1.3 fix adds a format === 'binary' guard that skips JSON.stringify and returns the raw base64 string as-is.
Problem 2 — NOT fixed in 1.1.3 ❌
_executeRequest decides how to wrap the body into a Blob based on whether a Content-Type: application/octet-stream header is present:
// runtimeDataClient.js — _executeRequest
const requestBody = config.body
? config.headers?.['Content-Type'] === 'application/octet-stream'
? new Blob([config.body], { type: 'application/octet-stream' })
: new Blob([config.body], { type: 'application/json' })
: '';
Because executeAsync never sets Content-Type: application/octet-stream for binary connector operations, _executeRequest always falls into the application/json branch. The raw base64 string is wrapped in a JSON blob and the Power Platform gateway forwards the ASCII content verbatim to SharePoint instead of binary bytes.
Additionally, retrieveDataAsync still JSON-serialises non-string bodies:
// runtimeDataClient.js — retrieveDataAsync (line 206)
body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
This means if a binary body is passed as a Uint8Array, it would be incorrectly serialised.
Why the old workaround for 1.1.1 broke on 1.1.3
Community patches (including my own) for 1.1.1 detected the double-JSON-stringified form by calling JSON.parse() on the body — if the parsed result was still a string, it decoded it to a Uint8Array. In 1.1.3, the value is no longer double-stringified, so JSON.parse("iVBOR...") throws a SyntaxError, the catch block silently discards it, and the binary passthrough never triggers.
Proposed fix
Two changes are needed in connectorDataOperationExecutor.js, plus one in runtimeDataClient.js:
- _buildOperationBodyParam — decode base64 → Uint8Array instead of returning a raw string:
// Before (1.1.3):
if (bodyParam.format === 'binary' && typeof value === 'string') {
return value; // still a string → _executeRequest uses application/json blob
}
// After:
if (bodyParam.format === 'binary' && typeof value === 'string') {
const binary = atob(value);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes; // Uint8Array → can be detected downstream to set octet-stream
}
- executeAsync — detect Uint8Array body and inject Content-Type: application/octet-stream:
const bodyParam = await this._buildOperationBody(operation, tableName);
const headers = await this._buildOperationHeader(operation, tableName);
// NEW: signal binary upload to _executeRequest
const resolvedHeaders = bodyParam instanceof Uint8Array
? { ...(headers ?? {}), 'Content-Type': 'application/octet-stream' }
: headers;
const result = await dataClient.retrieveDataAsync(
requestUrl, config.apiId, tableName, httpMethod, resolvedHeaders, bodyParam, { ... }
);
- retrieveDataAsync — pass Uint8Array/ArrayBuffer through without serialisation:
// Before:
body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
// After:
body: body
? (body instanceof Uint8Array || body instanceof ArrayBuffer
? body
: typeof body === 'string' ? body : JSON.stringify(body))
: undefined,
With these three changes, the flow becomes:
_buildOperationBodyParam → base64 decoded to Uint8Array
executeAsync → detects Uint8Array, adds Content-Type: application/octet-stream
retrieveDataAsync → passes Uint8Array through as-is
_executeRequest → sees Content-Type: application/octet-stream → creates a binary Blob → SharePoint receives actual binary bytes ✅
Reference:
#306 — original bug report
#230 — upstream tracking issue
Describe the bug
SharePointService.CreateFile() still saves corrupted content (raw base64 ASCII text) to SharePoint when uploading binary files (images, PDFs, etc.) after upgrading to @microsoft/power-apps 1.1.3, which was supposed to resolve this in issue #306 / #230.
Steps to Reproduce
Install @microsoft/power-apps@1.1.3
Call SharePointService.CreateFile(dataset, folderPath, 'photo.jpg', base64String) where base64String is a valid base64-encoded JPEG
Open the uploaded file in SharePoint — it is not a valid image; it contains raw base64 ASCII text
Additional context
The 1.1.3 fix was incomplete. It corrected only one of two problems:
Problem 1 — Fixed in 1.1.3 ✅
In _buildOperationBodyParam, the body value was previously always passed through JSON.stringify(), which double-encoded a base64 string into ""iVBOR..."". The 1.1.3 fix adds a format === 'binary' guard that skips JSON.stringify and returns the raw base64 string as-is.
Problem 2 — NOT fixed in 1.1.3 ❌
_executeRequest decides how to wrap the body into a Blob based on whether a Content-Type: application/octet-stream header is present:
// runtimeDataClient.js — _executeRequest
const requestBody = config.body
? config.headers?.['Content-Type'] === 'application/octet-stream'
? new Blob([config.body], { type: 'application/octet-stream' })
: new Blob([config.body], { type: 'application/json' })
: '';
Because executeAsync never sets Content-Type: application/octet-stream for binary connector operations, _executeRequest always falls into the application/json branch. The raw base64 string is wrapped in a JSON blob and the Power Platform gateway forwards the ASCII content verbatim to SharePoint instead of binary bytes.
Additionally, retrieveDataAsync still JSON-serialises non-string bodies:
// runtimeDataClient.js — retrieveDataAsync (line 206)
body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
This means if a binary body is passed as a Uint8Array, it would be incorrectly serialised.
Why the old workaround for 1.1.1 broke on 1.1.3
Community patches (including my own) for 1.1.1 detected the double-JSON-stringified form by calling JSON.parse() on the body — if the parsed result was still a string, it decoded it to a Uint8Array. In 1.1.3, the value is no longer double-stringified, so JSON.parse("iVBOR...") throws a SyntaxError, the catch block silently discards it, and the binary passthrough never triggers.
Proposed fix
Two changes are needed in connectorDataOperationExecutor.js, plus one in runtimeDataClient.js:
// Before (1.1.3):
if (bodyParam.format === 'binary' && typeof value === 'string') {
return value; // still a string → _executeRequest uses application/json blob
}
// After:
if (bodyParam.format === 'binary' && typeof value === 'string') {
const binary = atob(value);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes; // Uint8Array → can be detected downstream to set octet-stream
}
const bodyParam = await this._buildOperationBody(operation, tableName);
const headers = await this._buildOperationHeader(operation, tableName);
// NEW: signal binary upload to _executeRequest
const resolvedHeaders = bodyParam instanceof Uint8Array
? { ...(headers ?? {}), 'Content-Type': 'application/octet-stream' }
: headers;
const result = await dataClient.retrieveDataAsync(
requestUrl, config.apiId, tableName, httpMethod, resolvedHeaders, bodyParam, { ... }
);
// Before:
body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
// After:
body: body
? (body instanceof Uint8Array || body instanceof ArrayBuffer
? body
: typeof body === 'string' ? body : JSON.stringify(body))
: undefined,
With these three changes, the flow becomes:
_buildOperationBodyParam → base64 decoded to Uint8Array
executeAsync → detects Uint8Array, adds Content-Type: application/octet-stream
retrieveDataAsync → passes Uint8Array through as-is
_executeRequest → sees Content-Type: application/octet-stream → creates a binary Blob → SharePoint receives actual binary bytes ✅
Reference:
#306 — original bug report
#230 — upstream tracking issue