Search code examples
javascriptiosbackbone.js

iOS Keeps Logging me out of my Backbone Web Application


I hope someone can help me with this.

I have a Backbone based SPA for a responsive website with a .net WebAPI providing all of the data.

I've recently found a weird problem. I've added a search box, which searches one of the catalogues on the system. This works fine on desktop browsers and on Android. On iOS, executing a search seems to take you back to the sign in page.

You can execute a search in various ways, you can either hit enter or you can click the search icon. Both of these then trigger a method that navigates the router to the URL for the search result.

My first thought was that it was some button weirdness, but I don't think that's the problem as both methods of search execution are causing the same problem.

The search results are displayed in a view that is secured (It requires a username to be present - this is stored in a hidden field on the page). There are two search boxes on the site - one on the home page and one on the search results page itself (it shows a default set when you load it first time - which it does load first time fine). Both search boxes are exhibiting the same behaviour.

My site is set up in such a way that when Backbone pulls back a model, if it gets a 401 back from the API then it will send you back to the login page, so I can only think it's happening here.

Here's my view code...

    function (SiansPlan, ErrorManager, RecipeSearchResult, Header, Components, TemplateSource) {
    var recipeSearchView = SiansPlan.SecureView.extend({

        name: 'Recipe Search',
        sectionName: 'Recipes',
        queryText: '',

        template: Handlebars.compile(TemplateSource),

        headerView: new Header({ text: 'Recipes', swatch: 'e' }),
        searchBoxRegion: undefined,

        $searchWrapper: undefined,
        $queryHeaderMobile: undefined,
        $queryHeaderDesktop: undefined,
        $searchButton: undefined,
        $searchInput: undefined,
        $recipeSearch : undefined,

        events: {
            'click .link-container': 'showRecipe',
            'click #searchWrapper': 'showSearch',
            'click #searchButton': 'showOrPerformSearch',
            'keydown #searchButton': 'performSearchOnEnter',
            'keydown #recipeSearch': 'performSearchOnEnter'
        },

        initialize: function (options) {
            this.options = options || {};
            SiansPlan.SecureView.prototype.initialize.call(this, options);

            this.queryText = Object.exists(this.options.query) ? this.options.query : '';
        },

        bindData: function () {
            this.$el.html(this.template({ results: this.collection.toJSON() }));
        },

        render: function () {
            var that = this;

            if (this.isSecured()) {
                this.trigger('rendering');

                var params = {
                    success: function () {
                        that.bindData();
                        that.trigger('rendered');
                    },
                    error: function (model, xhr) {
                        if (Object.exists(xhr) && xhr.status == 401) {
                            that.applyTimedOutSecureLoginPrompt();
                        } else {
                            that.$el.html('Unable to fetch search results');
                            ErrorManager.handleXhr('Search failed', xhr);
                        }
                        that.trigger('rendered');
                    }
                };

                if (!Object.exists(this.collection)) {
                    this.collection = new RecipeSearchResult.Collection({ username: SiansPlanApp.session.username(), query: this.queryText });
                }

                this.collection.fetch(params);
            } else {
                this.applySecureLoginPrompt();
            }

            return this;
        },

        postRender: function () {
            var that = this;

            var queryHeader = "All recipes";
            if (Object.hasValue(this.queryText)) {
                queryHeader = this.collection.length + " results for '" + this.queryText + "'";
            }

            this.$searchWrapper = $('#searchWrapper');
            this.$queryHeaderMobile = $('#queryHeaderMobile');
            this.$queryHeaderDesktop = $('#queryHeaderDesktop');
            this.$searchButton = $('#searchWrapper');
            this.$searchInput = $('#searchInput');
            this.$recipeSearch = $('#recipeSearch');

            this.$queryHeaderMobile.html(queryHeader);
            this.$queryHeaderDesktop.html(queryHeader);
            this.$recipeSearch.val(this.queryText);

            SiansPlanApp.session.waitForLoad(30, function () {
                that.searchBoxRegion = new SiansPlan.Region({ el: '.recipe-search-box-container' });
                that.searchBoxRegion.renderView(new Components.RecipeSearchBox({ username: SiansPlanApp.session.username(), query: that.queryText, title: 'Search' }));
            });
        },

        performSearchOnEnter: function (e) {
            if (e.keyCode == 13) {
                this.showOrPerformSearch(e);
            }
        },

        showOrPerformSearch: function (e) {
            if (!this.$searchInput.is(':visible')) {
                this.showSearch(e);
            } else {
                e.preventDefault();
                var url = '/recipes/search/' + this.$recipeSearch.val();
                window.SiansPlanApp.router.navigate(url, true);
            }
            return false;
        },

        showRecipe: function (e) {
            e.preventDefault();
            var url = $(e.target).find('a').first().attr('href');
            window.SiansPlanApp.router.navigate(url, true);
        },

        showSearch: function (e) {
            e.preventDefault();
            if (!this.$searchInput.is(':visible')) {
                this.$queryHeaderMobile.hide();
                this.$searchInput.show();
                this.$recipeSearch.focus();
                this.$recipeSearch.select();
            }
            return false;
        }
    });

    return recipeSearchView;
});

UPDATES

I've set up some alerts as follows in the script to see what's going on and I've discovered the following...

    render: function () {
        var that = this;

        if (this.isSecured()) {
            this.trigger('rendering');

            var params = {
                success: function () {
                    alert('Bind has succeeded!');
                    that.bindData();
                    that.trigger('rendered');
                },
                error: function (model, xhr) {
                    alert('Bind has failed!');
                    if (Object.exists(xhr) && xhr.status == 401) {
                        that.applyTimedOutSecureLoginPrompt();
                    } else {
                        that.$el.html('Unable to fetch search results');
                        ErrorManager.handleXhr('Search failed', xhr);
                    }
                    that.trigger('rendered');
                    alert(xhr.status + ' ' + xhr.responseText);
                }
            };

            if (!Object.exists(this.collection)) {
                alert('Binding new collection: ' + SiansPlanApp.session.username() + ' - ' + this.queryText);
                this.collection = new RecipeSearchResult.Collection({ username: SiansPlanApp.session.username(), query: this.queryText });
            }

            alert('About to fetch using ' + this.collection.url());
            this.collection.fetch(params);
        } else {
            alert('I dont appear to be secured??');
            this.applySecureLoginPrompt();
        }

        return this;
    },
  1. When I first load the page (to show all the results) it loads fine and 'Bind Succeeded!' appears. The API call made is /api/recipes/search/{username}/

  2. When I submit search criteria it fails ('Bind failed!') with the API call of /api/recipes/search/{username}/{query} and returns a 401.

This has me even more befuddled than before as this now looks like an API issue, but other devices are working fine and if I submit the same queries into Fiddler everything is, as expected, fine.


Solution

  • I've found the answer in the smallest place...

    The issue was that the search criteria had an upper case letter. So, for example, when searching with 'Fish', The API generated a 301 which redirected to /api/recipes/search/{username}/fish. iOS didn't like that and reported it as a 401 (Which truly sucks!)