Skip to content

Commit 5ff1473

Browse files
committed
fix: System, unit, and conformance test cases
1 parent d4ce4b4 commit 5ff1473

7 files changed

Lines changed: 383 additions & 172 deletions

File tree

handwritten/storage/src/storage-transport.ts

Lines changed: 37 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,12 @@ export class StorageTransport {
145145
}
146146

147147
const urlString = reqOpts.url?.toString() || '';
148-
const isAbsolute = urlString.startsWith('http');
148+
const isAbsolute = this.#isValidUrl(urlString);
149+
150+
// Determine the base URL for the request
151+
const requestUrl = isAbsolute
152+
? urlString
153+
: new URL(urlString, this.baseUrl).toString();
149154

150155
try {
151156
const requestPromise = this.authClient.request<T>({
@@ -159,20 +164,22 @@ export class StorageTransport {
159164
},
160165
...reqOpts,
161166
data: reqOpts.body,
162-
params: isAbsolute ? undefined : reqOpts.queryParameters,
167+
params: reqOpts.queryParameters,
168+
paramsSerializer: this.#paramsSerializer,
163169
headers,
164-
url: isAbsolute
165-
? urlString
166-
: this.#buildUrl(reqOpts.url?.toString(), reqOpts.queryParameters),
170+
url: requestUrl,
167171
timeout: this.timeout,
168172
// eslint-disable-next-line @typescript-eslint/no-explicit-any
169173
...({decompress: false} as any),
170174
validateStatus: status => {
171-
if (urlString.includes('uploadType=resumable')) {
172-
return (status >= 200 && status < 300) || status === 308;
173-
}
174-
return status >= 200 && status < 300;
175-
}, //status => status >= 200 && status < 300,
175+
const isResumable =
176+
reqOpts.queryParameters?.uploadType === 'resumable' ||
177+
reqOpts.url?.toString().includes('uploadType=resumable');
178+
179+
return (
180+
(status >= 200 && status < 300) || (isResumable && status === 308)
181+
);
182+
},
176183
});
177184

178185
// Response Handling
@@ -218,25 +225,6 @@ export class StorageTransport {
218225
return finalHeaders;
219226
}
220227

221-
#buildUrl(pathUri = '', queryParameters: StorageQueryParameters = {}): URL {
222-
// Sync project ID in params if necessary
223-
if (
224-
'project' in queryParameters &&
225-
queryParameters.project !== this.projectId
226-
) {
227-
queryParameters.project = this.projectId;
228-
}
229-
230-
let url: URL;
231-
if (this.#isValidUrl(pathUri)) {
232-
url = new URL(pathUri);
233-
} else {
234-
url = new URL(pathUri, this.baseUrl); // Safer construction
235-
}
236-
url.search = this.#buildRequestQueryParams(queryParameters);
237-
return url;
238-
}
239-
240228
#isValidUrl(url: string): boolean {
241229
try {
242230
return Boolean(new URL(url));
@@ -245,6 +233,26 @@ export class StorageTransport {
245233
}
246234
}
247235

236+
/**
237+
* Serializes query parameters into a string.
238+
* Specifically handles arrays by appending each value individually
239+
* to satisfy GCS "repeated key" requirements (e.g., for IAM permissions).
240+
*/
241+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
242+
#paramsSerializer = (params: Record<string, any>): string => {
243+
const searchParams = new URLSearchParams();
244+
for (const [key, value] of Object.entries(params)) {
245+
if (value === undefined) continue;
246+
247+
if (Array.isArray(value)) {
248+
value.forEach(v => searchParams.append(key, String(v)));
249+
} else {
250+
searchParams.set(key, String(value));
251+
}
252+
}
253+
return searchParams.toString();
254+
};
255+
248256
#buildRequestHeaders(requestHeaders = {}) {
249257
const headers = new Headers(requestHeaders);
250258
headers.set('User-Agent', this.#getUserAgentString());
@@ -255,21 +263,6 @@ export class StorageTransport {
255263
return headers;
256264
}
257265

258-
#buildRequestQueryParams(queryParameters: StorageQueryParameters): string {
259-
const qp = new URLSearchParams();
260-
for (const [key, value] of Object.entries(queryParameters)) {
261-
if (value === undefined) continue;
262-
263-
if (Array.isArray(value)) {
264-
// This is the fix: append each item individually for repeated keys
265-
value.forEach(item => qp.append(key, String(item)));
266-
} else {
267-
qp.set(key, String(value));
268-
}
269-
}
270-
return qp.toString();
271-
}
272-
273266
#getUserAgentString(): string {
274267
const base = getUserAgentString();
275268
return this.providedUserAgent ? `${this.providedUserAgent} ${base}` : base;

handwritten/storage/src/storage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ export const RETRYABLE_ERR_FN_DEFAULT = function (err?: GaxiosError) {
390390
// Handle malformed responses or stream interruptions
391391
if (
392392
message.includes('unexpected end of json input') ||
393+
message.includes('unexpected token') ||
393394
message.includes('operation was aborted') ||
394395
message.includes('unexpected connection closure')
395396
) {

handwritten/storage/system-test/fixtures/index-cjs.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
// eslint-disable-next-line no-undef
15+
/* eslint-disable node/no-missing-require, no-unused-vars, no-undef */
1616
const {Storage} = require('@google-cloud/storage');
1717

1818
function main() {
19-
// eslint-disable-next-line no-unused-vars
2019
const storage = new Storage();
2120
}
2221

handwritten/storage/system-test/fixtures/index-esm.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
// eslint-disable-next-line no-undef
16-
const {Storage} = require('@google-cloud/storage');
15+
/* eslint-disable node/no-missing-import, no-unused-vars */
16+
import {Storage} from '@google-cloud/storage';
1717

1818
function main() {
19-
// eslint-disable-next-line no-unused-vars
2019
const storage = new Storage();
2120
}
2221

handwritten/storage/system-test/kitchen.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ describe('resumable-upload', () => {
5454
retryableErrorFn: RETRYABLE_ERR_FN_DEFAULT,
5555
};
5656

57-
const bucket = new Storage({retryOptions}).bucket(bucketName);
57+
const bucket = new Storage({
58+
projectId: process.env.PROJECT_ID,
59+
retryOptions: retryOptions,
60+
}).bucket(bucketName);
5861
let filePath: string;
5962

6063
before(async () => {
@@ -96,7 +99,7 @@ describe('resumable-upload', () => {
9699
// see: https://cloud.google.com/storage/docs/exponential-backoff:
97100
const ms = Math.pow(2, retries) * 1000 + Math.random() * 2000;
98101
console.info(`retrying "${title}" in ${ms}ms`);
99-
setTimeout(done(), ms);
102+
setTimeout(() => done, ms);
100103
}
101104

102105
it('should work', done => {

0 commit comments

Comments
 (0)