We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Spawner custom form validation
Are there ideas for allowing form validation for spawners that have a custom form?

I was thinking of raising an exception in `options_from_form()` and moving the `try` up by one line in [SpawnHandler](https://github.com/jupyter/jupyterhub/blob/master/jupyterhub/handlers/pages.py#L97).


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `jupyterhub/handlers/pages.py`
Content:
```
1 """Basic html-rendering handlers."""
2 
3 # Copyright (c) Jupyter Development Team.
4 # Distributed under the terms of the Modified BSD License.
5 
6 from tornado import web, gen
7 
8 from .. import orm
9 from ..utils import admin_only, url_path_join
10 from .base import BaseHandler
11 from .login import LoginHandler
12 
13 
14 class RootHandler(BaseHandler):
15     """Render the Hub root page.
16     
17     If logged in, redirects to:
18     
19     - single-user server if running
20     - hub home, otherwise
21     
22     Otherwise, renders login page.
23     """
24     def get(self):
25         user = self.get_current_user()
26         if user:
27             if user.running:
28                 url = user.server.base_url
29                 self.log.debug("User is running: %s", url)
30             else:
31                 url = url_path_join(self.hub.server.base_url, 'home')
32                 self.log.debug("User is not running: %s", url)
33             self.redirect(url)
34             return
35         url = url_path_join(self.hub.server.base_url, 'login')
36         self.redirect(url)
37 
38 
39 class HomeHandler(BaseHandler):
40     """Render the user's home page."""
41 
42     @web.authenticated
43     def get(self):
44         html = self.render_template('home.html',
45             user=self.get_current_user(),
46         )
47         self.finish(html)
48 
49 
50 class SpawnHandler(BaseHandler):
51     """Handle spawning of single-user servers via form.
52     
53     GET renders the form, POST handles form submission.
54     
55     Only enabled when Spawner.options_form is defined.
56     """
57     def _render_form(self, message=''):
58         user = self.get_current_user()
59         return self.render_template('spawn.html',
60             user=user,
61             spawner_options_form=user.spawner.options_form,
62             error_message=message,
63         )
64 
65     @web.authenticated
66     def get(self):
67         """GET renders form for spawning with user-specified options"""
68         user = self.get_current_user()
69         if user.running:
70             url = user.server.base_url
71             self.log.debug("User is running: %s", url)
72             self.redirect(url)
73             return
74         if user.spawner.options_form:
75             self.finish(self._render_form())
76         else:
77             # not running, no form. Trigger spawn.
78             url = url_path_join(self.base_url, 'user', user.name)
79             self.redirect(url)
80     
81     @web.authenticated
82     @gen.coroutine
83     def post(self):
84         """POST spawns with user-specified options"""
85         user = self.get_current_user()
86         if user.running:
87             url = user.server.base_url
88             self.log.warning("User is already running: %s", url)
89             self.redirect(url)
90             return
91         form_options = {}
92         for key, byte_list in self.request.body_arguments.items():
93             form_options[key] = [ bs.decode('utf8') for bs in byte_list ]
94         for key, byte_list in self.request.files.items():
95             form_options["%s_file"%key] = byte_list
96         options = user.spawner.options_from_form(form_options)
97         try:
98             yield self.spawn_single_user(user, options=options)
99         except Exception as e:
100             self.log.error("Failed to spawn single-user server with form", exc_info=True)
101             self.finish(self._render_form(str(e)))
102             return
103         self.set_login_cookie(user)
104         url = user.server.base_url
105         self.redirect(url)
106 
107 class AdminHandler(BaseHandler):
108     """Render the admin page."""
109 
110     @admin_only
111     def get(self):
112         available = {'name', 'admin', 'running', 'last_activity'}
113         default_sort = ['admin', 'name']
114         mapping = {
115             'running': '_server_id'
116         }
117         default_order = {
118             'name': 'asc',
119             'last_activity': 'desc',
120             'admin': 'desc',
121             'running': 'desc',
122         }
123         sorts = self.get_arguments('sort') or default_sort
124         orders = self.get_arguments('order')
125         
126         for bad in set(sorts).difference(available):
127             self.log.warn("ignoring invalid sort: %r", bad)
128             sorts.remove(bad)
129         for bad in set(orders).difference({'asc', 'desc'}):
130             self.log.warn("ignoring invalid order: %r", bad)
131             orders.remove(bad)
132         
133         # add default sort as secondary
134         for s in default_sort:
135             if s not in sorts:
136                 sorts.append(s)
137         if len(orders) < len(sorts):
138             for col in sorts[len(orders):]:
139                 orders.append(default_order[col])
140         else:
141             orders = orders[:len(sorts)]
142         
143         # this could be one incomprehensible nested list comprehension
144         # get User columns
145         cols = [ getattr(orm.User, mapping.get(c, c)) for c in sorts ]
146         # get User.col.desc() order objects
147         ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ]
148         
149         users = self.db.query(orm.User).order_by(*ordered)
150         users = [ self._user_from_orm(u) for u in users ]
151         running = [ u for u in users if u.running ]
152         
153         html = self.render_template('admin.html',
154             user=self.get_current_user(),
155             admin_access=self.settings.get('admin_access', False),
156             users=users,
157             running=running,
158             sort={s:o for s,o in zip(sorts, orders)},
159         )
160         self.finish(html)
161 
162 
163 default_handlers = [
164     (r'/', RootHandler),
165     (r'/home', HomeHandler),
166     (r'/admin', AdminHandler),
167     (r'/spawn', SpawnHandler),
168 ]
169
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py
--- a/jupyterhub/handlers/pages.py
+++ b/jupyterhub/handlers/pages.py
@@ -93,8 +93,8 @@
             form_options[key] = [ bs.decode('utf8') for bs in byte_list ]
         for key, byte_list in self.request.files.items():
             form_options["%s_file"%key] = byte_list
-        options = user.spawner.options_from_form(form_options)
         try:
+            options = user.spawner.options_from_form(form_options)
             yield self.spawn_single_user(user, options=options)
         except Exception as e:
             self.log.error("Failed to spawn single-user server with form", exc_info=True)

{"golden_diff": "diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py\n--- a/jupyterhub/handlers/pages.py\n+++ b/jupyterhub/handlers/pages.py\n@@ -93,8 +93,8 @@\n             form_options[key] = [ bs.decode('utf8') for bs in byte_list ]\n         for key, byte_list in self.request.files.items():\n             form_options[\"%s_file\"%key] = byte_list\n-        options = user.spawner.options_from_form(form_options)\n         try:\n+            options = user.spawner.options_from_form(form_options)\n             yield self.spawn_single_user(user, options=options)\n         except Exception as e:\n             self.log.error(\"Failed to spawn single-user server with form\", exc_info=True)\n", "issue": "Spawner custom form validation\nAre there ideas for allowing form validation for spawners that have a custom form?\n\nI was thinking of raising an exception in `options_from_form()` and moving the `try` up by one line in [SpawnHandler](https://github.com/jupyter/jupyterhub/blob/master/jupyterhub/handlers/pages.py#L97).\n\n", "before_files": [{"content": "\"\"\"Basic html-rendering handlers.\"\"\"\n\n# Copyright (c) Jupyter Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nfrom tornado import web, gen\n\nfrom .. import orm\nfrom ..utils import admin_only, url_path_join\nfrom .base import BaseHandler\nfrom .login import LoginHandler\n\n\nclass RootHandler(BaseHandler):\n    \"\"\"Render the Hub root page.\n    \n    If logged in, redirects to:\n    \n    - single-user server if running\n    - hub home, otherwise\n    \n    Otherwise, renders login page.\n    \"\"\"\n    def get(self):\n        user = self.get_current_user()\n        if user:\n            if user.running:\n                url = user.server.base_url\n                self.log.debug(\"User is running: %s\", url)\n            else:\n                url = url_path_join(self.hub.server.base_url, 'home')\n                self.log.debug(\"User is not running: %s\", url)\n            self.redirect(url)\n            return\n        url = url_path_join(self.hub.server.base_url, 'login')\n        self.redirect(url)\n\n\nclass HomeHandler(BaseHandler):\n    \"\"\"Render the user's home page.\"\"\"\n\n    @web.authenticated\n    def get(self):\n        html = self.render_template('home.html',\n            user=self.get_current_user(),\n        )\n        self.finish(html)\n\n\nclass SpawnHandler(BaseHandler):\n    \"\"\"Handle spawning of single-user servers via form.\n    \n    GET renders the form, POST handles form submission.\n    \n    Only enabled when Spawner.options_form is defined.\n    \"\"\"\n    def _render_form(self, message=''):\n        user = self.get_current_user()\n        return self.render_template('spawn.html',\n            user=user,\n            spawner_options_form=user.spawner.options_form,\n            error_message=message,\n        )\n\n    @web.authenticated\n    def get(self):\n        \"\"\"GET renders form for spawning with user-specified options\"\"\"\n        user = self.get_current_user()\n        if user.running:\n            url = user.server.base_url\n            self.log.debug(\"User is running: %s\", url)\n            self.redirect(url)\n            return\n        if user.spawner.options_form:\n            self.finish(self._render_form())\n        else:\n            # not running, no form. Trigger spawn.\n            url = url_path_join(self.base_url, 'user', user.name)\n            self.redirect(url)\n    \n    @web.authenticated\n    @gen.coroutine\n    def post(self):\n        \"\"\"POST spawns with user-specified options\"\"\"\n        user = self.get_current_user()\n        if user.running:\n            url = user.server.base_url\n            self.log.warning(\"User is already running: %s\", url)\n            self.redirect(url)\n            return\n        form_options = {}\n        for key, byte_list in self.request.body_arguments.items():\n            form_options[key] = [ bs.decode('utf8') for bs in byte_list ]\n        for key, byte_list in self.request.files.items():\n            form_options[\"%s_file\"%key] = byte_list\n        options = user.spawner.options_from_form(form_options)\n        try:\n            yield self.spawn_single_user(user, options=options)\n        except Exception as e:\n            self.log.error(\"Failed to spawn single-user server with form\", exc_info=True)\n            self.finish(self._render_form(str(e)))\n            return\n        self.set_login_cookie(user)\n        url = user.server.base_url\n        self.redirect(url)\n\nclass AdminHandler(BaseHandler):\n    \"\"\"Render the admin page.\"\"\"\n\n    @admin_only\n    def get(self):\n        available = {'name', 'admin', 'running', 'last_activity'}\n        default_sort = ['admin', 'name']\n        mapping = {\n            'running': '_server_id'\n        }\n        default_order = {\n            'name': 'asc',\n            'last_activity': 'desc',\n            'admin': 'desc',\n            'running': 'desc',\n        }\n        sorts = self.get_arguments('sort') or default_sort\n        orders = self.get_arguments('order')\n        \n        for bad in set(sorts).difference(available):\n            self.log.warn(\"ignoring invalid sort: %r\", bad)\n            sorts.remove(bad)\n        for bad in set(orders).difference({'asc', 'desc'}):\n            self.log.warn(\"ignoring invalid order: %r\", bad)\n            orders.remove(bad)\n        \n        # add default sort as secondary\n        for s in default_sort:\n            if s not in sorts:\n                sorts.append(s)\n        if len(orders) < len(sorts):\n            for col in sorts[len(orders):]:\n                orders.append(default_order[col])\n        else:\n            orders = orders[:len(sorts)]\n        \n        # this could be one incomprehensible nested list comprehension\n        # get User columns\n        cols = [ getattr(orm.User, mapping.get(c, c)) for c in sorts ]\n        # get User.col.desc() order objects\n        ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ]\n        \n        users = self.db.query(orm.User).order_by(*ordered)\n        users = [ self._user_from_orm(u) for u in users ]\n        running = [ u for u in users if u.running ]\n        \n        html = self.render_template('admin.html',\n            user=self.get_current_user(),\n            admin_access=self.settings.get('admin_access', False),\n            users=users,\n            running=running,\n            sort={s:o for s,o in zip(sorts, orders)},\n        )\n        self.finish(html)\n\n\ndefault_handlers = [\n    (r'/', RootHandler),\n    (r'/home', HomeHandler),\n    (r'/admin', AdminHandler),\n    (r'/spawn', SpawnHandler),\n]\n", "path": "jupyterhub/handlers/pages.py"}], "after_files": [{"content": "\"\"\"Basic html-rendering handlers.\"\"\"\n\n# Copyright (c) Jupyter Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nfrom tornado import web, gen\n\nfrom .. import orm\nfrom ..utils import admin_only, url_path_join\nfrom .base import BaseHandler\nfrom .login import LoginHandler\n\n\nclass RootHandler(BaseHandler):\n    \"\"\"Render the Hub root page.\n    \n    If logged in, redirects to:\n    \n    - single-user server if running\n    - hub home, otherwise\n    \n    Otherwise, renders login page.\n    \"\"\"\n    def get(self):\n        user = self.get_current_user()\n        if user:\n            if user.running:\n                url = user.server.base_url\n                self.log.debug(\"User is running: %s\", url)\n            else:\n                url = url_path_join(self.hub.server.base_url, 'home')\n                self.log.debug(\"User is not running: %s\", url)\n            self.redirect(url)\n            return\n        url = url_path_join(self.hub.server.base_url, 'login')\n        self.redirect(url)\n\n\nclass HomeHandler(BaseHandler):\n    \"\"\"Render the user's home page.\"\"\"\n\n    @web.authenticated\n    def get(self):\n        html = self.render_template('home.html',\n            user=self.get_current_user(),\n        )\n        self.finish(html)\n\n\nclass SpawnHandler(BaseHandler):\n    \"\"\"Handle spawning of single-user servers via form.\n    \n    GET renders the form, POST handles form submission.\n    \n    Only enabled when Spawner.options_form is defined.\n    \"\"\"\n    def _render_form(self, message=''):\n        user = self.get_current_user()\n        return self.render_template('spawn.html',\n            user=user,\n            spawner_options_form=user.spawner.options_form,\n            error_message=message,\n        )\n\n    @web.authenticated\n    def get(self):\n        \"\"\"GET renders form for spawning with user-specified options\"\"\"\n        user = self.get_current_user()\n        if user.running:\n            url = user.server.base_url\n            self.log.debug(\"User is running: %s\", url)\n            self.redirect(url)\n            return\n        if user.spawner.options_form:\n            self.finish(self._render_form())\n        else:\n            # not running, no form. Trigger spawn.\n            url = url_path_join(self.base_url, 'user', user.name)\n            self.redirect(url)\n    \n    @web.authenticated\n    @gen.coroutine\n    def post(self):\n        \"\"\"POST spawns with user-specified options\"\"\"\n        user = self.get_current_user()\n        if user.running:\n            url = user.server.base_url\n            self.log.warning(\"User is already running: %s\", url)\n            self.redirect(url)\n            return\n        form_options = {}\n        for key, byte_list in self.request.body_arguments.items():\n            form_options[key] = [ bs.decode('utf8') for bs in byte_list ]\n        for key, byte_list in self.request.files.items():\n            form_options[\"%s_file\"%key] = byte_list\n        try:\n            options = user.spawner.options_from_form(form_options)\n            yield self.spawn_single_user(user, options=options)\n        except Exception as e:\n            self.log.error(\"Failed to spawn single-user server with form\", exc_info=True)\n            self.finish(self._render_form(str(e)))\n            return\n        self.set_login_cookie(user)\n        url = user.server.base_url\n        self.redirect(url)\n\nclass AdminHandler(BaseHandler):\n    \"\"\"Render the admin page.\"\"\"\n\n    @admin_only\n    def get(self):\n        available = {'name', 'admin', 'running', 'last_activity'}\n        default_sort = ['admin', 'name']\n        mapping = {\n            'running': '_server_id'\n        }\n        default_order = {\n            'name': 'asc',\n            'last_activity': 'desc',\n            'admin': 'desc',\n            'running': 'desc',\n        }\n        sorts = self.get_arguments('sort') or default_sort\n        orders = self.get_arguments('order')\n        \n        for bad in set(sorts).difference(available):\n            self.log.warn(\"ignoring invalid sort: %r\", bad)\n            sorts.remove(bad)\n        for bad in set(orders).difference({'asc', 'desc'}):\n            self.log.warn(\"ignoring invalid order: %r\", bad)\n            orders.remove(bad)\n        \n        # add default sort as secondary\n        for s in default_sort:\n            if s not in sorts:\n                sorts.append(s)\n        if len(orders) < len(sorts):\n            for col in sorts[len(orders):]:\n                orders.append(default_order[col])\n        else:\n            orders = orders[:len(sorts)]\n        \n        # this could be one incomprehensible nested list comprehension\n        # get User columns\n        cols = [ getattr(orm.User, mapping.get(c, c)) for c in sorts ]\n        # get User.col.desc() order objects\n        ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ]\n        \n        users = self.db.query(orm.User).order_by(*ordered)\n        users = [ self._user_from_orm(u) for u in users ]\n        running = [ u for u in users if u.running ]\n        \n        html = self.render_template('admin.html',\n            user=self.get_current_user(),\n            admin_access=self.settings.get('admin_access', False),\n            users=users,\n            running=running,\n            sort={s:o for s,o in zip(sorts, orders)},\n        )\n        self.finish(html)\n\n\ndefault_handlers = [\n    (r'/', RootHandler),\n    (r'/home', HomeHandler),\n    (r'/admin', AdminHandler),\n    (r'/spawn', SpawnHandler),\n]\n", "path": "jupyterhub/handlers/pages.py"}]}