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

Fix double-visits after modal form submission redirects #60

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jayohms
Copy link
Contributor

@jayohms jayohms commented Dec 16, 2024

This resolves the latter part of issue #12 where double-requests were being made to the /success page after a modal form submission.

To replicate in the demo app:

  1. Navigate to "Load a page modally"
  2. Tap "Submit Form"
  3. Observe that two /success GET requests are made to the server

Here are the relevant logs:

[Bridge] ← formSubmissionStarted ["timestamp": 1734382774207, "location": http://localhost:45678/new]
[Bridge] ← formSubmissionFinished ["location": http://localhost:45678/new, "timestamp": 1734382774231]
[Bridge] ← visitProposed ["options": {
    action = advance;
    response =     {
        redirected = 1;
        responseHTML = "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">\n    <meta name=\"turbo-refresh-method\" content=\"morph\">\n    <meta name=\"turbo-refresh-scroll\" content=\"preserve\">\n    <meta name=\"turbo-prefetch\" content=\"false\">\n\n    <title>It Worked!</title>\n\n    <link rel=\"stylesheet\" href=\"/styles/base.css\">\n    <link rel=\"stylesheet\" href=\"/styles/utilities.css\">\n    <link rel=\"stylesheet\" href=\"/styles/app.css\">\n\n    \n      <link rel=\"stylesheet\" href=\"/styles/native.css\">\n      <link rel=\"stylesheet\" href=\"/styles/bridge.css\">\n    \n\n    <script type=\"importmap\">\n      {\n        \"imports\": {\n          \"@hotwired/turbo\": \"https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm\",\n          \"@hotwired/stimulus\": \"https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm\",\n          \"@hotwired/hotwire-native-bridge\": \"https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm\"\n        }\n      }\n    </script>\n\n    <script type=\"module\" src=\"/application.js\"></script>\n  </head>\n  <body class=\"\">\n    <main id=\"content\" class=\"grid pad --bottom-soft\">\n      <h1 class=\"page-title\">It Worked!</h1>\n\n<p>\n  You have successfully submitted a form. What a ride.\n</p>\n\n    </main>\n  </body>\n</html>\n";
        statusCode = 200;
    };
    shouldCacheSnapshot = 0;
}, "location": http://localhost:45678/success, "timestamp": 1734382774232]
[Session] visit ["reload": false, "options": HotwireNative.VisitOptions(action: HotwireNative.VisitAction.advance, response: Optional(HotwireNative.VisitResponse(statusCode: 200, responseHTML: Optional("<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">\n    <meta name=\"turbo-refresh-method\" content=\"morph\">\n    <meta name=\"turbo-refresh-scroll\" content=\"preserve\">\n    <meta name=\"turbo-prefetch\" content=\"false\">\n\n    <title>It Worked!</title>\n\n    <link rel=\"stylesheet\" href=\"/styles/base.css\">\n    <link rel=\"stylesheet\" href=\"/styles/utilities.css\">\n    <link rel=\"stylesheet\" href=\"/styles/app.css\">\n\n    \n      <link rel=\"stylesheet\" href=\"/styles/native.css\">\n      <link rel=\"stylesheet\" href=\"/styles/bridge.css\">\n    \n\n    <script type=\"importmap\">\n      {\n        \"imports\": {\n          \"@hotwired/turbo\": \"https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm\",\n          \"@hotwired/stimulus\": \"https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm\",\n          \"@hotwired/hotwire-native-bridge\": \"https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm\"\n        }\n      }\n    </script>\n\n    <script type=\"module\" src=\"/application.js\"></script>\n  </head>\n  <body class=\"\">\n    <main id=\"content\" class=\"grid pad --bottom-soft\">\n      <h1 class=\"page-title\">It Worked!</h1>\n\n<p>\n  You have successfully submitted a form. What a ride.\n</p>\n\n    </main>\n  </body>\n</html>\n"), redirected: true))), "location": http://localhost:45678/success]
[JavascriptVisit] startVisit http://localhost:45678/success, [:]
[Bridge] → window.turboNative.visitLocationWithOptionsAndRestorationIdentifier [Optional("http://localhost:45678/success"), Optional({
    action = advance;
    response =     {
        redirected = 1;
        responseHTML = "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">\n    <meta name=\"turbo-refresh-method\" content=\"morph\">\n    <meta name=\"turbo-refresh-scroll\" content=\"preserve\">\n    <meta name=\"turbo-prefetch\" content=\"false\">\n\n    <title>It Worked!</title>\n\n    <link rel=\"stylesheet\" href=\"/styles/base.css\">\n    <link rel=\"stylesheet\" href=\"/styles/utilities.css\">\n    <link rel=\"stylesheet\" href=\"/styles/app.css\">\n\n    \n      <link rel=\"stylesheet\" href=\"/styles/native.css\">\n      <link rel=\"stylesheet\" href=\"/styles/bridge.css\">\n    \n\n    <script type=\"importmap\">\n      {\n        \"imports\": {\n          \"@hotwired/turbo\": \"https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm\",\n          \"@hotwired/stimulus\": \"https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm\",\n          \"@hotwired/hotwire-native-bridge\": \"https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm\"\n        }\n      }\n    </script>\n\n    <script type=\"module\" src=\"/application.js\"></script>\n  </head>\n  <body class=\"\">\n    <main id=\"content\" class=\"grid pad --bottom-soft\">\n      <h1 class=\"page-title\">It Worked!</h1>\n\n<p>\n  You have successfully submitted a form. What a ride.\n</p>\n\n    </main>\n  </body>\n</html>\n";
        statusCode = 200;
    };
}), nil]
[Bridge] ← visitStarted ["timestamp": 1734382774242, "isPageRefresh": 0, "identifier": de24db3b-3d36-4232-90b2-db4a117873e6, "hasCachedSnapshot": 1]
[JavascriptVisit] didStartVisitWithIdentifier http://localhost:45678/success, ["identifier": "de24db3b-3d36-4232-90b2-db4a117873e6", "hasCachedSnapshot": true, "isPageRefresh": false]
[Bridge] ← visitRequestStarted ["timestamp": 1734382774242, "identifier": de24db3b-3d36-4232-90b2-db4a117873e6]
[JavascriptVisit] didStartRequestForVisitWithIdentifier http://localhost:45678/success, ["identifier": "de24db3b-3d36-4232-90b2-db4a117873e6", "date": 2024-12-16 20:59:34 +0000]
[Bridge] ← visitRequestCompleted ["identifier": de24db3b-3d36-4232-90b2-db4a117873e6, "timestamp": 1734382774242]
[JavascriptVisit] didCompleteRequestForVisitWithIdentifier http://localhost:45678/success, ["identifier": "de24db3b-3d36-4232-90b2-db4a117873e6"]
[Bridge] ← visitRequestFinished ["timestamp": 1734382774242, "identifier": de24db3b-3d36-4232-90b2-db4a117873e6]
[JavascriptVisit] didFinishRequestForVisitWithIdentifier http://localhost:45678/success, ["identifier": "de24db3b-3d36-4232-90b2-db4a117873e6", "date": 2024-12-16 20:59:34 +0000]
[Bridge] = window.turboNative.visitLocationWithOptionsAndRestorationIdentifier evaluation complete
[Bridge] ← visitCompleted ["identifier": de24db3b-3d36-4232-90b2-db4a117873e6, "timestamp": 1734382774244, "restorationIdentifier": 3bcd4a82-040e-4bad-8e93-c1d6b230a231]
[JavascriptVisit] didCompleteVisitWithIdentifier http://localhost:45678/success, ["restorationIdentifier": "3bcd4a82-040e-4bad-8e93-c1d6b230a231", "identifier": "de24db3b-3d36-4232-90b2-db4a117873e6"]
[Bridge] bridgeDestinationViewWillDisappear: http://localhost:45678/new
[Bridge] → window.turboNative.cacheSnapshot []
[Bridge] bridgeDestinationViewDidDisappear: http://localhost:45678/new
[Bridge] bridgeDestinationViewDidLoad: http://localhost:45678/success
[Bridge] bridgeDestinationViewWillDisappear: http://localhost:45678
[Bridge] → window.turboNative.clearSnapshotCache []
[Session] visit ["options": HotwireNative.VisitOptions(action: HotwireNative.VisitAction.restore, response: nil), "location": http://localhost:45678/success, "reload": false]
[JavascriptVisit] startVisit http://localhost:45678/success, [:]
[Bridge] → window.turboNative.visitLocationWithOptionsAndRestorationIdentifier [Optional("http://localhost:45678/success"), Optional({
    action = restore;
}), Optional("3bcd4a82-040e-4bad-8e93-c1d6b230a231")]
[Bridge] bridgeDestinationViewWillAppear: http://localhost:45678/success
[Bridge] = window.turboNative.cacheSnapshot evaluation complete
[Bridge] = window.turboNative.clearSnapshotCache evaluation complete
[Bridge] ← visitStarted ["isPageRefresh": 1, "identifier": 0a488ae2-1ae5-45b7-9a42-c54c45de8822, "hasCachedSnapshot": 0, "timestamp": 1734382774772]
[JavascriptVisit] didStartVisitWithIdentifier http://localhost:45678/success, ["identifier": "0a488ae2-1ae5-45b7-9a42-c54c45de8822", "hasCachedSnapshot": false, "isPageRefresh": true]
[Bridge] ← visitRequestStarted ["timestamp": 1734382774775, "identifier": 0a488ae2-1ae5-45b7-9a42-c54c45de8822]
[JavascriptVisit] didStartRequestForVisitWithIdentifier http://localhost:45678/success, ["identifier": "0a488ae2-1ae5-45b7-9a42-c54c45de8822", "date": 2024-12-16 20:59:34 +0000]
[Bridge] = window.turboNative.visitLocationWithOptionsAndRestorationIdentifier evaluation complete
[Bridge] ← visitRendered ["identifier": de24db3b-3d36-4232-90b2-db4a117873e6, "timestamp": 1734382774777]
[Bridge] ← visitRendered ["timestamp": 1734382774777, "identifier": de24db3b-3d36-4232-90b2-db4a117873e6]
[Bridge] ← visitRequestCompleted ["timestamp": 1734382774783, "identifier": 0a488ae2-1ae5-45b7-9a42-c54c45de8822]
[JavascriptVisit] didCompleteRequestForVisitWithIdentifier http://localhost:45678/success, ["identifier": "0a488ae2-1ae5-45b7-9a42-c54c45de8822"]
[Bridge] ← visitRequestFinished ["identifier": 0a488ae2-1ae5-45b7-9a42-c54c45de8822, "timestamp": 1734382774783]
[JavascriptVisit] didFinishRequestForVisitWithIdentifier http://localhost:45678/success, ["date": 2024-12-16 20:59:34 +0000, "identifier": "0a488ae2-1ae5-45b7-9a42-c54c45de8822"]
[Bridge] ← visitCompleted ["restorationIdentifier": 3bcd4a82-040e-4bad-8e93-c1d6b230a231, "identifier": 0a488ae2-1ae5-45b7-9a42-c54c45de8822, "timestamp": 1734382774805]
[JavascriptVisit] didCompleteVisitWithIdentifier http://localhost:45678/success, ["identifier": "0a488ae2-1ae5-45b7-9a42-c54c45de8822", "restorationIdentifier": "3bcd4a82-040e-4bad-8e93-c1d6b230a231"]
[Bridge] ← visitRendered ["identifier": 0a488ae2-1ae5-45b7-9a42-c54c45de8822, "timestamp": 1734382774850]
[JavascriptVisit] didRenderForVisitWithIdentifier http://localhost:45678/success, ["identifier": "0a488ae2-1ae5-45b7-9a42-c54c45de8822"]
[Bridge] bridgeDestinationViewDidDisappear: http://localhost:45678
[Bridge] bridgeDestinationViewDidAppear: http://localhost:45678/success

Note how the first visit properly carries the response.responseHTML from the form submission redirect along to the javascript adapter:

[Bridge] → window.turboNative.visitLocationWithOptionsAndRestorationIdentifier [Optional("http://localhost:45678/success"), Optional({
    action = advance;
    response =     {
        redirected = 1;
        responseHTML = "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">\n    <meta name=\"turbo-refresh-method\" content=\"morph\">\n    <meta name=\"turbo-refresh-scroll\" content=\"preserve\">\n    <meta name=\"turbo-prefetch\" content=\"false\">\n\n    <title>It Worked!</title>\n\n    <link rel=\"stylesheet\" href=\"/styles/base.css\">\n    <link rel=\"stylesheet\" href=\"/styles/utilities.css\">\n    <link rel=\"stylesheet\" href=\"/styles/app.css\">\n\n    \n      <link rel=\"stylesheet\" href=\"/styles/native.css\">\n      <link rel=\"stylesheet\" href=\"/styles/bridge.css\">\n    \n\n    <script type=\"importmap\">\n      {\n        \"imports\": {\n          \"@hotwired/turbo\": \"https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm\",\n          \"@hotwired/stimulus\": \"https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm\",\n          \"@hotwired/hotwire-native-bridge\": \"https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm\"\n        }\n      }\n    </script>\n\n    <script type=\"module\" src=\"/application.js\"></script>\n  </head>\n  <body class=\"\">\n    <main id=\"content\" class=\"grid pad --bottom-soft\">\n      <h1 class=\"page-title\">It Worked!</h1>\n\n<p>\n  You have successfully submitted a form. What a ride.\n</p>\n\n    </main>\n  </body>\n</html>\n";
        statusCode = 200;
    };
}), nil]

The subsequent second visit(which shouldn't happen) is a restore visit:

[Bridge] → window.turboNative.visitLocationWithOptionsAndRestorationIdentifier [Optional("http://localhost:45678/success"), Optional({
    action = restore;
}), Optional("3bcd4a82-040e-4bad-8e93-c1d6b230a231")]

The restore visit was being accidentally triggered by falling through a condition in the Session where the currentVisit.state was already completed (instead of started) since the initial visit carried the responseHTML and didn't need to hit the network to finish the request.

…ontext -> default context when the visit carries response.responseHTML
Copy link
Member

@joemasilotti joemasilotti left a comment

Choose a reason for hiding this comment

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

Amazing - this has bugged me for months!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants