Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cross-origin redirect delegate #203

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Docs/Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ The default `Action` is `.advance`. In most cases you’ll respond to an advance

When you follow a link annotated with `data-turbo-action="replace"`, the proposed Action will be `.replace`. Usually you’ll want to handle a replace visit by replacing the top-most visible view controller with a new one instead of pushing.

### Responding to cross-origin redirects

When a Visit action results in a cross-origin redirect that Turbo iOS is not able to handle due to the cross-origin nature, your Session's delegate can handle this by implementing the `session:didProposeVisitToCrossOriginRedirect`. Depending on your navigation structure, you could choose to pop the view controller and open the URL in a an external browser, for example:

```swift
navigationController.popViewController(animated: false)
UIApplication.shared.open(url)
Comment on lines +112 to +113
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
navigationController.popViewController(animated: false)
UIApplication.shared.open(url)
func session(_ session: Session, didProposeVisitToCrossOriginRedirect url: URL) {
navigationController.popViewController(animated: false)
UIApplication.shared.open(url)
}

```

## Handling Failed Requests

Turbo iOS calls the `session:didFailRequestForVisitable:error:` method when a visit request fails. This might be because of a network error, or because the server returned an HTTP 4xx or 5xx status code. If it was a network error in the main cold boot visit, it will be the `NSError` returned by WebKit. If it was a HTTP error or a network error from a JavaScript visit the error will be a `TurboError` and you can retrieve the status code.
Expand Down
4 changes: 4 additions & 0 deletions Source/Session/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@ extension Session: WebViewDelegate {
delegate?.session(self, didProposeVisit: proposal)
}

func webView(_ bridge: WebViewBridge, didProposeVisitToCrossOriginRedirect location: URL) {
delegate?.session(self, didProposeVisitToCrossOriginRedirect: location)
}

func webView(_ webView: WebViewBridge, didStartFormSubmissionToLocation location: URL) {
delegate?.sessionDidStartFormSubmission(self)
}
Expand Down
3 changes: 3 additions & 0 deletions Source/Session/SessionDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import UIKit

public protocol SessionDelegate: AnyObject {
func session(_ session: Session, didProposeVisit proposal: VisitProposal)
func session(_ session: Session, didProposeVisitToCrossOriginRedirect url: URL)
func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, error: Error)

func session(_ session: Session, openExternalURL url: URL)
Expand All @@ -24,6 +25,8 @@ public extension SessionDelegate {
func session(_ session: Session, openExternalURL url: URL) {
UIApplication.shared.open(url)
}

func session(_ session: Session, didProposeVisitToCrossOriginRedirect url: URL) {}

func sessionDidStartRequest(_ session: Session) {}
func sessionDidFinishRequest(_ session: Session) {}
Expand Down
1 change: 1 addition & 0 deletions Source/WebView/ScriptMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ extension ScriptMessage {
case pageLoadFailed
case errorRaised
case visitProposed
case visitProposedToCrossOriginRedirect
case visitProposalScrollingToAnchor
case visitProposalRefreshingPage
case visitStarted
Expand Down
3 changes: 3 additions & 0 deletions Source/WebView/WebViewBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import WebKit

protocol WebViewDelegate: AnyObject {
func webView(_ webView: WebViewBridge, didProposeVisitToLocation location: URL, options: VisitOptions)
func webView(_ webView: WebViewBridge, didProposeVisitToCrossOriginRedirect location: URL)
func webViewDidInvalidatePage(_ webView: WebViewBridge)
func webView(_ webView: WebViewBridge, didStartFormSubmissionToLocation location: URL)
func webView(_ webView: WebViewBridge, didFinishFormSubmissionToLocation location: URL)
Expand Down Expand Up @@ -129,6 +130,8 @@ extension WebViewBridge: ScriptMessageHandlerDelegate {
delegate?.webViewDidInvalidatePage(self)
case .visitProposed:
delegate?.webView(self, didProposeVisitToLocation: message.location!, options: message.options!)
case .visitProposedToCrossOriginRedirect:
delegate?.webView(self, didProposeVisitToCrossOriginRedirect: message.location!)
case .visitProposalScrollingToAnchor:
break
case .visitProposalRefreshingPage:
Expand Down
29 changes: 27 additions & 2 deletions Source/WebView/turbo.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,18 @@
this.loadResponseForVisitWithIdentifier(visit.identifier)
}

visitRequestFailedWithStatusCode(visit, statusCode) {
this.postMessage("visitRequestFailed", { identifier: visit.identifier, statusCode: statusCode })
async visitRequestFailedWithStatusCode(visit, statusCode) {
// Turbo does not permit cross-origin fetch redirect attempts and
// they'll lead to a visit request failure. Attempt to see if the
// visit request failure was due to a cross-origin redirect.
const redirect = await this.fetchFailedRequestCrossOriginRedirect(visit, statusCode)
const location = visit.location.toString()

if (redirect != null) {
this.postMessage("visitProposedToCrossOriginRedirect", { location: redirect.toString(), identifier: visit.identifier })
} else {
this.postMessage("visitRequestFailed", { location: location, identifier: visit.identifier, statusCode: statusCode })
}
}

visitRequestFinished(visit) {
Expand Down Expand Up @@ -174,6 +184,21 @@
}

// Private

async fetchFailedRequestCrossOriginRedirect(visit, statusCode) {
// Non-HTTP status codes are sent by Turbo for network
// failures, including cross-origin fetch redirect attempts.
if (statusCode <= 0) {
try {
const response = await fetch(visit.location, { redirect: "follow" })
if (response.url != null && response.url.origin != visit.location.origin) {
return response.url
}
} catch {}
}

return null
}

postMessage(name, data = {}) {
data["timestamp"] = Date.now()
Expand Down