Skip to content

Commit c02aaf7

Browse files
JosiasMBgoogsvg
andauthored
fix(http): use addEncodedPathSegments to stop double-encoding path ID (#330)
Co-authored-by: Gordan Ovcaric <gordan.ovcaric@hotmail.com>
1 parent 2c33e4b commit c02aaf7

44 files changed

Lines changed: 857 additions & 382 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Nylas Java SDK Changelog
22

3+
## [unreleased]
4+
5+
### Fixed
6+
* Prevent double-encoding of pre-encoded path IDs when building request URLs, fixing Gmail attachment download 404s for attachment IDs containing reserved characters such as `:` and `=`
7+
38
## [v2.17.0] - Release 2026-06-15
49

510
### Added

src/main/kotlin/com/nylas/NylasClient.kt

Lines changed: 121 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,18 @@ open class NylasClient(
215215
queryParams: IQueryParams? = null,
216216
overrides: RequestOverrides? = null,
217217
): T {
218-
val url = buildUrl(path, queryParams, overrides)
218+
val url = buildRawUrl(path, queryParams, overrides)
219+
return executeRequest(url, HttpMethod.GET, null, resultType, overrides)
220+
}
221+
222+
@Throws(AbstractNylasApiError::class, NylasSdkTimeoutError::class)
223+
internal open fun <T> executeGetEncoded(
224+
path: String,
225+
resultType: Type,
226+
queryParams: IQueryParams? = null,
227+
overrides: RequestOverrides? = null,
228+
): T {
229+
val url = buildEncodedUrl(path, queryParams, overrides)
219230
return executeRequest(url, HttpMethod.GET, null, resultType, overrides)
220231
}
221232

@@ -236,7 +247,20 @@ open class NylasClient(
236247
queryParams: IQueryParams? = null,
237248
overrides: RequestOverrides? = null,
238249
): T {
239-
val url = buildUrl(path, queryParams, overrides)
250+
val url = buildRawUrl(path, queryParams, overrides)
251+
val jsonBody = if (requestBody != null) JsonHelper.jsonRequestBody(requestBody) else null
252+
return executeRequest(url, HttpMethod.PUT, jsonBody, resultType, overrides)
253+
}
254+
255+
@Throws(AbstractNylasApiError::class, NylasSdkTimeoutError::class)
256+
internal open fun <T> executePutEncoded(
257+
path: String,
258+
resultType: Type,
259+
requestBody: String? = null,
260+
queryParams: IQueryParams? = null,
261+
overrides: RequestOverrides? = null,
262+
): T {
263+
val url = buildEncodedUrl(path, queryParams, overrides)
240264
val jsonBody = if (requestBody != null) JsonHelper.jsonRequestBody(requestBody) else null
241265
return executeRequest(url, HttpMethod.PUT, jsonBody, resultType, overrides)
242266
}
@@ -258,7 +282,20 @@ open class NylasClient(
258282
queryParams: IQueryParams? = null,
259283
overrides: RequestOverrides? = null,
260284
): T {
261-
val url = buildUrl(path, queryParams, overrides)
285+
val url = buildRawUrl(path, queryParams, overrides)
286+
val jsonBody = if (requestBody != null) JsonHelper.jsonRequestBody(requestBody) else null
287+
return executeRequest(url, HttpMethod.PATCH, jsonBody, resultType, overrides)
288+
}
289+
290+
@Throws(AbstractNylasApiError::class, NylasSdkTimeoutError::class)
291+
internal open fun <T> executePatchEncoded(
292+
path: String,
293+
resultType: Type,
294+
requestBody: String? = null,
295+
queryParams: IQueryParams? = null,
296+
overrides: RequestOverrides? = null,
297+
): T {
298+
val url = buildEncodedUrl(path, queryParams, overrides)
262299
val jsonBody = if (requestBody != null) JsonHelper.jsonRequestBody(requestBody) else null
263300
return executeRequest(url, HttpMethod.PATCH, jsonBody, resultType, overrides)
264301
}
@@ -280,7 +317,23 @@ open class NylasClient(
280317
queryParams: IQueryParams? = null,
281318
overrides: RequestOverrides? = null,
282319
): T {
283-
val url = buildUrl(path, queryParams, overrides)
320+
val url = buildRawUrl(path, queryParams, overrides)
321+
var jsonBody = ByteArray(0).toRequestBody(null)
322+
if (requestBody != null) {
323+
jsonBody = JsonHelper.jsonRequestBody(requestBody)
324+
}
325+
return executeRequest(url, HttpMethod.POST, jsonBody, resultType, overrides)
326+
}
327+
328+
@Throws(AbstractNylasApiError::class, NylasSdkTimeoutError::class)
329+
internal open fun <T> executePostEncoded(
330+
path: String,
331+
resultType: Type,
332+
requestBody: String? = null,
333+
queryParams: IQueryParams? = null,
334+
overrides: RequestOverrides? = null,
335+
): T {
336+
val url = buildEncodedUrl(path, queryParams, overrides)
284337
var jsonBody = ByteArray(0).toRequestBody(null)
285338
if (requestBody != null) {
286339
jsonBody = JsonHelper.jsonRequestBody(requestBody)
@@ -303,7 +356,18 @@ open class NylasClient(
303356
queryParams: IQueryParams? = null,
304357
overrides: RequestOverrides? = null,
305358
): T {
306-
val url = buildUrl(path, queryParams, overrides)
359+
val url = buildRawUrl(path, queryParams, overrides)
360+
return executeRequest(url, HttpMethod.DELETE, null, resultType, overrides)
361+
}
362+
363+
@Throws(AbstractNylasApiError::class, NylasSdkTimeoutError::class)
364+
internal open fun <T> executeDeleteEncoded(
365+
path: String,
366+
resultType: Type,
367+
queryParams: IQueryParams? = null,
368+
overrides: RequestOverrides? = null,
369+
): T {
370+
val url = buildEncodedUrl(path, queryParams, overrides)
307371
return executeRequest(url, HttpMethod.DELETE, null, resultType, overrides)
308372
}
309373

@@ -324,7 +388,20 @@ open class NylasClient(
324388
queryParams: IQueryParams? = null,
325389
overrides: RequestOverrides? = null,
326390
): T {
327-
val url = buildUrl(path, queryParams, overrides)
391+
val url = buildRawUrl(path, queryParams, overrides)
392+
val jsonBody = JsonHelper.jsonRequestBody(requestBody)
393+
return executeRequest(url, HttpMethod.DELETE, jsonBody, resultType, overrides)
394+
}
395+
396+
@Throws(AbstractNylasApiError::class, NylasSdkTimeoutError::class)
397+
internal open fun <T> executeDeleteEncoded(
398+
path: String,
399+
resultType: Type,
400+
requestBody: String,
401+
queryParams: IQueryParams? = null,
402+
overrides: RequestOverrides? = null,
403+
): T {
404+
val url = buildEncodedUrl(path, queryParams, overrides)
328405
val jsonBody = JsonHelper.jsonRequestBody(requestBody)
329406
return executeRequest(url, HttpMethod.DELETE, jsonBody, resultType, overrides)
330407
}
@@ -348,7 +425,20 @@ open class NylasClient(
348425
queryParams: IQueryParams? = null,
349426
overrides: RequestOverrides? = null,
350427
): T {
351-
val url = buildUrl(path, queryParams, overrides)
428+
val url = buildRawUrl(path, queryParams, overrides)
429+
return executeRequest(url, method, requestBody, resultType, overrides)
430+
}
431+
432+
@Throws(AbstractNylasApiError::class, NylasSdkTimeoutError::class)
433+
internal open fun <T> executeFormRequestEncoded(
434+
path: String,
435+
method: HttpMethod,
436+
requestBody: RequestBody,
437+
resultType: Type,
438+
queryParams: IQueryParams? = null,
439+
overrides: RequestOverrides? = null,
440+
): T {
441+
val url = buildEncodedUrl(path, queryParams, overrides)
352442
return executeRequest(url, method, requestBody, resultType, overrides)
353443
}
354444

@@ -406,7 +496,17 @@ open class NylasClient(
406496
queryParams: IQueryParams? = null,
407497
overrides: RequestOverrides? = null,
408498
): ResponseBody {
409-
val url = buildUrl(path, queryParams, overrides)
499+
val url = buildRawUrl(path, queryParams, overrides)
500+
return this.executeRequestRawResponse(url, HttpMethod.GET, null, overrides)
501+
}
502+
503+
@Throws(AbstractNylasApiError::class, NylasSdkTimeoutError::class)
504+
internal open fun downloadResponseEncoded(
505+
path: String,
506+
queryParams: IQueryParams? = null,
507+
overrides: RequestOverrides? = null,
508+
): ResponseBody {
509+
val url = buildEncodedUrl(path, queryParams, overrides)
410510
return this.executeRequestRawResponse(url, HttpMethod.GET, null, overrides)
411511
}
412512

@@ -526,14 +626,25 @@ open class NylasClient(
526626
)
527627
}
528628

529-
private fun buildUrl(path: String, queryParams: IQueryParams?, overrides: RequestOverrides?): HttpUrl.Builder {
530-
// Sets the API URI if it is provided in the overrides.
629+
private fun buildRawUrl(path: String, queryParams: IQueryParams?, overrides: RequestOverrides?): HttpUrl.Builder {
531630
var url = if (overrides?.apiUri != null) {
532631
overrides.apiUri.toHttpUrl().newBuilder().addPathSegments(path)
533632
} else {
534633
newUrlBuilder().addPathSegments(path)
535634
}
635+
if (queryParams != null) {
636+
url = addQueryParams(url, queryParams.convertToMap())
637+
}
536638

639+
return url
640+
}
641+
642+
private fun buildEncodedUrl(path: String, queryParams: IQueryParams?, overrides: RequestOverrides?): HttpUrl.Builder {
643+
var url = if (overrides?.apiUri != null) {
644+
overrides.apiUri.toHttpUrl().newBuilder().addEncodedPathSegments(path)
645+
} else {
646+
newUrlBuilder().addEncodedPathSegments(path)
647+
}
537648
if (queryParams != null) {
538649
url = addQueryParams(url, queryParams.convertToMap())
539650
}

src/main/kotlin/com/nylas/resources/Attachments.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ class Attachments(client: NylasClient) : Resource<Attachment>(client, Attachment
2424
@Throws(NylasOAuthError::class, NylasSdkTimeoutError::class)
2525
@JvmOverloads
2626
fun find(identifier: String, attachmentId: String, queryParams: FindAttachmentQueryParams, overrides: RequestOverrides? = null): Response<Attachment> {
27-
val path = String.format("v3/grants/%s/attachments/%s", identifier, PathEncoder.encode(attachmentId))
28-
return findResource(path, queryParams, overrides = overrides)
27+
val path = String.format("v3/grants/%s/attachments/%s", PathEncoder.encode(identifier), PathEncoder.encode(attachmentId))
28+
return findResourceEncoded(path, queryParams, overrides = overrides)
2929
}
3030

3131
/**
@@ -46,9 +46,9 @@ class Attachments(client: NylasClient) : Resource<Attachment>(client, Attachment
4646
@Throws(NylasOAuthError::class, NylasSdkTimeoutError::class)
4747
@JvmOverloads
4848
fun download(identifier: String, attachmentId: String, queryParams: FindAttachmentQueryParams, overrides: RequestOverrides? = null): ResponseBody {
49-
val path = String.format("v3/grants/%s/attachments/%s/download", identifier, PathEncoder.encode(attachmentId))
49+
val path = String.format("v3/grants/%s/attachments/%s/download", PathEncoder.encode(identifier), PathEncoder.encode(attachmentId))
5050

51-
return client.downloadResponse(path, queryParams, overrides = overrides)
51+
return client.downloadResponseEncoded(path, queryParams, overrides = overrides)
5252
}
5353

5454
/**

src/main/kotlin/com/nylas/resources/Bookings.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class Bookings(client: NylasClient) : Resource<Booking>(client, Booking::class.j
2727
overrides: RequestOverrides? = null,
2828
): Response<Booking> {
2929
val path = String.format("v3/scheduling/bookings/%s", PathEncoder.encode(bookingId))
30-
return findResource(path, queryParams, overrides = overrides)
30+
return findResourceEncoded(path, queryParams, overrides = overrides)
3131
}
3232

3333
/**
@@ -47,7 +47,7 @@ class Bookings(client: NylasClient) : Resource<Booking>(client, Booking::class.j
4747
val path = "v3/scheduling/bookings"
4848
val adapter = JsonHelper.moshi().adapter(CreateBookingRequest::class.java)
4949
val serializedRequestBody = adapter.toJson(requestBody)
50-
return createResource(path, serializedRequestBody, queryParams, overrides = overrides)
50+
return createResourceEncoded(path, serializedRequestBody, queryParams, overrides = overrides)
5151
}
5252

5353
/**
@@ -69,7 +69,7 @@ class Bookings(client: NylasClient) : Resource<Booking>(client, Booking::class.j
6969
val path = String.format("v3/scheduling/bookings/%s", PathEncoder.encode(bookingId))
7070
val adapter = JsonHelper.moshi().adapter(ConfirmBookingRequest::class.java)
7171
val serializedRequestBody = adapter.toJson(requestBody)
72-
return updateResource(path, serializedRequestBody, queryParams, overrides = overrides)
72+
return updateResourceEncoded(path, serializedRequestBody, queryParams, overrides = overrides)
7373
}
7474

7575
/**
@@ -91,7 +91,7 @@ class Bookings(client: NylasClient) : Resource<Booking>(client, Booking::class.j
9191
val path = String.format("v3/scheduling/bookings/%s", PathEncoder.encode(bookingId))
9292
val adapter = JsonHelper.moshi().adapter(RescheduleBookingRequest::class.java)
9393
val serializedRequestBody = adapter.toJson(requestBody)
94-
return patchResource(path, serializedRequestBody, queryParams, overrides = overrides)
94+
return patchResourceEncoded(path, serializedRequestBody, queryParams, overrides = overrides)
9595
}
9696

9797
/**
@@ -136,7 +136,7 @@ class Bookings(client: NylasClient) : Resource<Booking>(client, Booking::class.j
136136
val path = String.format("v3/scheduling/bookings/%s", PathEncoder.encode(bookingId))
137137
val adapter = JsonHelper.moshi().adapter(DestroyBookingRequest::class.java)
138138
val serializedRequestBody = adapter.toJson(requestBody)
139-
return client.executeDelete(path, DeleteResponse::class.java, serializedRequestBody, queryParams, overrides)
139+
return client.executeDeleteEncoded(path, DeleteResponse::class.java, serializedRequestBody, queryParams, overrides)
140140
}
141141

142142
/**
@@ -153,6 +153,6 @@ class Bookings(client: NylasClient) : Resource<Booking>(client, Booking::class.j
153153
*/
154154
fun destroy(bookingId: String, queryParams: DestroyBookingQueryParams? = null, overrides: RequestOverrides? = null): DeleteResponse {
155155
val path = String.format("v3/scheduling/bookings/%s", PathEncoder.encode(bookingId))
156-
return destroyResource(path, queryParams, overrides = overrides)
156+
return destroyResourceEncoded(path, queryParams, overrides = overrides)
157157
}
158158
}

src/main/kotlin/com/nylas/resources/Calendars.kt

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ class Calendars(client: NylasClient) : Resource<Calendar>(client, Calendar::clas
2626
@Throws(NylasApiError::class, NylasSdkTimeoutError::class)
2727
@JvmOverloads
2828
fun list(identifier: String, queryParams: ListCalendersQueryParams? = null, overrides: RequestOverrides? = null): ListResponse<Calendar> {
29-
val path = String.format("v3/grants/%s/calendars", identifier)
30-
return listResource(path, queryParams, overrides)
29+
val path = String.format("v3/grants/%s/calendars", PathEncoder.encode(identifier))
30+
return listResourceEncoded(path, queryParams, overrides)
3131
}
3232

3333
/**
@@ -40,8 +40,8 @@ class Calendars(client: NylasClient) : Resource<Calendar>(client, Calendar::clas
4040
@Throws(NylasApiError::class, NylasSdkTimeoutError::class)
4141
@JvmOverloads
4242
fun find(identifier: String, calendarId: String, overrides: RequestOverrides? = null): Response<Calendar> {
43-
val path = String.format("v3/grants/%s/calendars/%s", identifier, PathEncoder.encode(calendarId))
44-
return findResource(path, overrides = overrides)
43+
val path = String.format("v3/grants/%s/calendars/%s", PathEncoder.encode(identifier), PathEncoder.encode(calendarId))
44+
return findResourceEncoded(path, overrides = overrides)
4545
}
4646

4747
/**
@@ -54,10 +54,10 @@ class Calendars(client: NylasClient) : Resource<Calendar>(client, Calendar::clas
5454
@Throws(NylasApiError::class, NylasSdkTimeoutError::class)
5555
@JvmOverloads
5656
fun create(identifier: String, requestBody: CreateCalendarRequest, overrides: RequestOverrides? = null): Response<Calendar> {
57-
val path = String.format("v3/grants/%s/calendars", identifier)
57+
val path = String.format("v3/grants/%s/calendars", PathEncoder.encode(identifier))
5858
val adapter = JsonHelper.moshi().adapter(CreateCalendarRequest::class.java)
5959
val serializedRequestBody = adapter.toJson(requestBody)
60-
return createResource(path, serializedRequestBody, overrides = overrides)
60+
return createResourceEncoded(path, serializedRequestBody, overrides = overrides)
6161
}
6262

6363
/**
@@ -71,10 +71,10 @@ class Calendars(client: NylasClient) : Resource<Calendar>(client, Calendar::clas
7171
@Throws(NylasApiError::class, NylasSdkTimeoutError::class)
7272
@JvmOverloads
7373
fun update(identifier: String, calendarId: String, requestBody: UpdateCalendarRequest, overrides: RequestOverrides? = null): Response<Calendar> {
74-
val path = String.format("v3/grants/%s/calendars/%s", identifier, PathEncoder.encode(calendarId))
74+
val path = String.format("v3/grants/%s/calendars/%s", PathEncoder.encode(identifier), PathEncoder.encode(calendarId))
7575
val adapter = JsonHelper.moshi().adapter(UpdateCalendarRequest::class.java)
7676
val serializedRequestBody = adapter.toJson(requestBody)
77-
return updateResource(path, serializedRequestBody, overrides = overrides)
77+
return updateResourceEncoded(path, serializedRequestBody, overrides = overrides)
7878
}
7979

8080
/**
@@ -86,8 +86,8 @@ class Calendars(client: NylasClient) : Resource<Calendar>(client, Calendar::clas
8686
*/
8787
@Throws(NylasApiError::class, NylasSdkTimeoutError::class)
8888
fun destroy(identifier: String, calendarId: String, overrides: RequestOverrides? = null): DeleteResponse {
89-
val path = String.format("v3/grants/%s/calendars/%s", identifier, PathEncoder.encode(calendarId))
90-
return destroyResource(path, overrides = overrides)
89+
val path = String.format("v3/grants/%s/calendars/%s", PathEncoder.encode(identifier), PathEncoder.encode(calendarId))
90+
return destroyResourceEncoded(path, overrides = overrides)
9191
}
9292

9393
/**
@@ -107,7 +107,7 @@ class Calendars(client: NylasClient) : Resource<Calendar>(client, Calendar::clas
107107

108108
val responseType = Types.newParameterizedType(Response::class.java, GetAvailabilityResponse::class.java)
109109

110-
return client.executePost(path, responseType, serializedRequestBody, overrides = overrides)
110+
return client.executePostEncoded(path, responseType, serializedRequestBody, overrides = overrides)
111111
}
112112

113113
/**
@@ -120,14 +120,14 @@ class Calendars(client: NylasClient) : Resource<Calendar>(client, Calendar::clas
120120
@Throws(NylasApiError::class, NylasSdkTimeoutError::class)
121121
@JvmOverloads
122122
fun getFreeBusy(identifier: String, request: GetFreeBusyRequest, overrides: RequestOverrides? = null): Response<List<GetFreeBusyResponse>> {
123-
val path = String.format("v3/grants/%s/calendars/free-busy", identifier)
123+
val path = String.format("v3/grants/%s/calendars/free-busy", PathEncoder.encode(identifier))
124124

125125
val serializedRequestBody = JsonHelper.moshi()
126126
.adapter(GetFreeBusyRequest::class.java)
127127
.toJson(request)
128128

129129
val responseType = Types.newParameterizedType(Response::class.java, GET_FREE_BUSY_RESPONSE_ADAPTER)
130130

131-
return client.executePost(path, responseType, serializedRequestBody, overrides = overrides)
131+
return client.executePostEncoded(path, responseType, serializedRequestBody, overrides = overrides)
132132
}
133133
}

0 commit comments

Comments
 (0)