Skip to content

Commit

Permalink
[Deprecation]: Remove internal objects from public API
Browse files Browse the repository at this point in the history
The `FetchRequest`, `FetchResponse`, and `FormSubmission` objects are
internal abstractions that facilitate Turbo's management of HTTP and
Form Submission life cycles.

Currently, references to instances of those classes are available
through `event.detail` for the following events:

* `turbo:frame-render`
* `turbo:before-fetch-request`
* `turbo:before-fetch-response`
* `turbo:submit-start`
* `turbo:submit-end`

Similarly, the `turbo:before-fetch-request` exposes a `fetchOptions`
object that is a separate instance from the one used to submit the
request. This means that **any** modifications to **any** value made
from within an event listener are ineffective and **do not change** the
ensuing request.

This commit deprecates those properties in favor of their built-in
foundations, namely:

* [Request][]
* [Response][]

The properties that expose those instances will remain as deprecations,
but will be inaccessible after an `8.0` release.

[Request]: https://developer.mozilla.org/en-US/docs/Web/API/Request
[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
  • Loading branch information
seanpdoyle committed Sep 12, 2023
1 parent 4c006dc commit 6994399
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 86 deletions.
92 changes: 67 additions & 25 deletions src/core/drive/form_submission.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,64 +116,106 @@ export class FormSubmission {

// Fetch request delegate

prepareRequest(request) {
if (!request.isSafe) {
prepareRequest(fetchRequest) {
if (!fetchRequest.isSafe) {
const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token")
if (token) {
request.headers["X-CSRF-Token"] = token
fetchRequest.headers.set("X-CSRF-Token", token)
}
}

if (this.requestAcceptsTurboStreamResponse(request)) {
request.acceptResponseType(StreamMessage.contentType)
if (this.requestAcceptsTurboStreamResponse(fetchRequest)) {
fetchRequest.acceptResponseType(StreamMessage.contentType)
}
}

requestStarted(_request) {
requestStarted(fetchRequest) {
const formSubmission = this

this.state = FormSubmissionState.waiting
this.submitter?.setAttribute("disabled", "")
this.setSubmitsWith()
dispatch("turbo:submit-start", {
target: this.formElement,
detail: { formSubmission: this }
detail: {
request: fetchRequest.request,
submitter: this.submitter,

get formSubmission() {
console.warn("`event.detail.formSubmission` is deprecated. Use `event.target`, `event.detail.submitter`, and `event.detail.request` instead")

return formSubmission
}
}
})
this.delegate.formSubmissionStarted(this)
}

requestPreventedHandlingResponse(request, response) {
this.result = { success: response.succeeded, fetchResponse: response }
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {
this.result = { success: fetchResponse.succeeded, fetchResponse: fetchResponse }
}

requestSucceededWithResponse(request, response) {
if (response.clientError || response.serverError) {
this.delegate.formSubmissionFailedWithResponse(this, response)
} else if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
requestSucceededWithResponse(fetchRequest, fetchResponse) {
if (fetchResponse.clientError || fetchResponse.serverError) {
this.delegate.formSubmissionFailedWithResponse(this, fetchResponse)
} else if (this.requestMustRedirect(fetchRequest) && responseSucceededWithoutRedirect(fetchResponse)) {
const error = new Error("Form responses must redirect to another location")
this.delegate.formSubmissionErrored(this, error)
} else {
this.state = FormSubmissionState.receiving
this.result = { success: true, fetchResponse: response }
this.delegate.formSubmissionSucceededWithResponse(this, response)
this.result = {
success: true,
response: fetchResponse.response,

get fetchResponse() {
console.warn("`event.detail.fetchResponse` is deprecated. Use `event.detail.response` instead")

return fetchResponse
}
}
this.delegate.formSubmissionSucceededWithResponse(this, fetchResponse)
}
}

requestFailedWithResponse(request, response) {
this.result = { success: false, fetchResponse: response }
this.delegate.formSubmissionFailedWithResponse(this, response)
requestFailedWithResponse(fetchRequest, fetchResponse) {
this.result = {
success: false,
response: fetchResponse.response,

get fetchResponse() {
console.warn("`event.detail.fetchResponse` is deprecated. Use `event.detail.response` instead")

return fetchResponse
}
}
this.delegate.formSubmissionFailedWithResponse(this, fetchResponse)
}

requestErrored(request, error) {
requestErrored(fetchRequest, error) {
this.result = { success: false, error }
this.delegate.formSubmissionErrored(this, error)
}

requestFinished(_request) {
requestFinished(fetchRequest) {
this.state = FormSubmissionState.stopped
this.submitter?.removeAttribute("disabled")
this.resetSubmitterText()
const { formSubmission } = this

dispatch("turbo:submit-end", {
target: this.formElement,
detail: { formSubmission: this, ...this.result }
detail: {
request: fetchRequest.request,
submitter: this.submitter,

get formSubmission() {
console.warn("`event.detail.formSubmission` is deprecated. Use `event.target`, `event.detail.submitter`, and `event.detail.request` instead")

return formSubmission
},

...this.result
}
})
this.delegate.formSubmissionFinished(this)
}
Expand Down Expand Up @@ -204,12 +246,12 @@ export class FormSubmission {
}
}

requestMustRedirect(request) {
return !request.isSafe && this.mustRedirect
requestMustRedirect(fetchRequest) {
return !fetchRequest.isSafe && this.mustRedirect
}

requestAcceptsTurboStreamResponse(request) {
return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement)
requestAcceptsTurboStreamResponse(fetchRequest) {
return !fetchRequest.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement)
}

get submitsWith() {
Expand Down
2 changes: 1 addition & 1 deletion src/core/frames/frame_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export class FrameController {
// Fetch request delegate

prepareRequest(request) {
request.headers["Turbo-Frame"] = this.id
request.headers.set("Turbo-Frame", this.id)

if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) {
request.acceptResponseType(StreamMessage.contentType)
Expand Down
10 changes: 9 additions & 1 deletion src/core/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,15 @@ export class Session {

notifyApplicationAfterFrameRender(fetchResponse, frame) {
return dispatch("turbo:frame-render", {
detail: { fetchResponse },
detail: {
response: fetchResponse.response,

get fetchResponse() {
console.warn("`event.detail.fetchResponse` is deprecated. Use `event.detail.response` instead")

return fetchResponse
}
},
target: frame,
cancelable: true
})
Expand Down
77 changes: 63 additions & 14 deletions src/http/fetch_request.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FetchResponse } from "./fetch_response"
import { dispatch } from "../util"
import { expandURL } from "../core/url"

export function fetchMethodFromString(method) {
switch (method.toLowerCase()) {
Expand All @@ -24,17 +25,46 @@ export const FetchMethod = {
delete: "delete"
}

export function isSafe(method) {
return fetchMethodFromString(method) === FetchMethod.get
}

export class FetchRequest {
abortController = new AbortController()
#resolveRequestPromise = (_value) => {}

constructor(delegate, method, location, body = new URLSearchParams(), target = null) {
method = fetchMethodFromString(method)

const url = expandURL(location)

this.delegate = delegate
this.method = method
this.headers = this.defaultHeaders
this.body = body
this.url = location
this.target = target
this.request = new Request(url.href, {
method,
body: isSafe(method) ? null : body,
credentials: "same-origin",
redirect: "follow",
referrer: this.delegate.referrer?.href,
signal: this.abortController.signal,
headers: this.defaultHeaders
})
}

get method() {
return this.request.method
}

get headers() {
return this.request.headers
}

get body() {
return this.request.body
}

get url() {
return this.request.url
}

get location() {
Expand All @@ -59,7 +89,7 @@ export class FetchRequest {
await this.#allowRequestToBeIntercepted(fetchOptions)
try {
this.delegate.requestStarted(this)
const response = await fetch(this.url.href, fetchOptions)
const response = await fetch(this.request)
return await this.receive(response)
} catch (error) {
if (error.name !== "AbortError") {
Expand All @@ -77,7 +107,14 @@ export class FetchRequest {
const fetchResponse = new FetchResponse(response)
const event = dispatch("turbo:before-fetch-response", {
cancelable: true,
detail: { fetchResponse },
detail: {
get fetchResponse() {
console.warn("`event.detail.fetchResponse` is deprecated. Use `event.detail.response` instead")

return fetchResponse
},
response
},
target: this.target
})
if (event.defaultPrevented) {
Expand All @@ -92,9 +129,9 @@ export class FetchRequest {

get fetchOptions() {
return {
method: FetchMethod[this.method].toUpperCase(),
method: this.method,
credentials: "same-origin",
headers: this.headers,
headers: Object.fromEntries(this.headers.entries()),
redirect: "follow",
body: this.isSafe ? null : this.body,
signal: this.abortSignal,
Expand All @@ -109,24 +146,36 @@ export class FetchRequest {
}

get isSafe() {
return this.method === FetchMethod.get
return isSafe(this.method)
}

get abortSignal() {
return this.abortController.signal
return this.request.signal
}

acceptResponseType(mimeType) {
this.headers["Accept"] = [mimeType, this.headers["Accept"]].join(", ")
this.headers.set("Accept", [mimeType, this.headers.get("Accept")].join(", "))
}

async #allowRequestToBeIntercepted(fetchOptions) {
const { request } = this
const requestInterception = new Promise((resolve) => (this.#resolveRequestPromise = resolve))
const event = dispatch("turbo:before-fetch-request", {
cancelable: true,
detail: {
fetchOptions,
url: this.url,
get fetchOptions() {
console.warn("`event.detail.fetchOptions` is deprecated. Use `event.detail.request` instead")

return fetchOptions
},

get url() {
console.warn("`event.detail.url` is deprecated. Use `event.detail.request.url` instead")

return request.url
},

request: this.request,
resume: this.#resolveRequestPromise
},
target: this.target
Expand All @@ -138,7 +187,7 @@ export class FetchRequest {
const event = dispatch("turbo:fetch-request-error", {
target: this.target,
cancelable: true,
detail: { request: this, error: error }
detail: { request: this.request, error: error }
})

return !event.defaultPrevented
Expand Down
2 changes: 1 addition & 1 deletion src/tests/fixtures/form.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html id="html" data-skip-event-details="turbo:submit-start turbo:submit-end turbo:fetch-request-error">
<html id="html" data-skip-event-details="turbo:fetch-request-error">
<head>
<meta charset="utf-8">
<title>Form</title>
Expand Down
2 changes: 1 addition & 1 deletion src/tests/fixtures/navigation.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html id="html" data-skip-event-details="turbo:submit-start turbo:submit-end">
<html id="html">
<head>
<meta charset="utf-8">
<meta name="csp-nonce" content="123">
Expand Down
2 changes: 1 addition & 1 deletion src/tests/fixtures/stream.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html data-skip-event-details="turbo:submit-start turbo:submit-end">
<html>
<head>
<meta charset="utf-8">
<title>Turbo Streams</title>
Expand Down
14 changes: 14 additions & 0 deletions src/tests/fixtures/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@
returned[key] = value.toJSON()
} else if (value instanceof Element) {
returned[key] = value.outerHTML
} else if (value instanceof Headers) {
returned[key] = Object.fromEntries(value.entries())
} else if (value instanceof Request) {
const { method, url, headers } = value

returned[key] = serializeToChannel({ method, url, headers })
} else if (value instanceof Response) {
const { url, status, headers } = value

returned[key] = serializeToChannel({ url, status, headers })
} else if (value instanceof AbortSignal) {
returned[key] = "cannot encode AbortSignal instance"
} else if (key === "formSubmission") {
returned[key] = "cannot encode FormSubmission instance"
} else if (typeof value == "object") {
if (visited.has(value)) {
returned[key] = "skipped to prevent infinitely recursing"
Expand Down
Loading

0 comments on commit 6994399

Please sign in to comment.