Skip to content
This repository was archived by the owner on Mar 16, 2021. It is now read-only.

Commit 55abb0b

Browse files
memorymxschmitt
authored andcommitted
Handle missing links gracefully. (#112)
Herein, we do two things: 1- implement a custom handler for the virtual filesystem that, rather than returning a simple (and ugly) 404 page, redirects the client back to the root URL with the `customUrl` query parameter filled out with the value of the request path. 2- In home.js, if the `customUrl` param is filled out, automatically select the `custom` state setting, and pre-fill out the CustomID input field with the value of that param. In short, the server will never again return a 404 error, but instead will gracefully prompt the user to fill in the missing link.
1 parent 8f770c7 commit 55abb0b

File tree

3 files changed

+86
-20
lines changed

3 files changed

+86
-20
lines changed

internal/handlers/handlers.go

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,65 @@ func (h *Handler) setHandlers() error {
166166

167167
// Handling the shorted URLs, if no one exists, it checks
168168
// in the filesystem and sets headers for caching
169-
h.engine.NoRoute(h.handleAccess, func(c *gin.Context) {
170-
c.Header("Vary", "Accept-Encoding")
171-
c.Header("Cache-Control", "public, max-age=2592000")
172-
c.Header("ETag", util.VersionInfo.Commit)
173-
}, gin.WrapH(http.FileServer(FS(false))))
169+
h.engine.NoRoute(
170+
h.handleAccess, // look up shortcuts
171+
func(c *gin.Context) { // no shortcut found, prep response for FS
172+
c.Header("Vary", "Accept-Encoding")
173+
c.Header("Cache-Control", "public, max-age=2592000")
174+
c.Header("ETag", util.VersionInfo.Commit)
175+
},
176+
// Pass down to the embedded FS, but let 404s escape via
177+
// the interceptHandler.
178+
gin.WrapH(interceptHandler(http.FileServer(FS(false)), customErrorHandler)),
179+
// not in FS; redirect to root with customURL target filled out
180+
func(c *gin.Context) {
181+
// if we get to this point we should not let the client cache
182+
c.Header("Cache-Control", "no-cache, no-store")
183+
c.Redirect(http.StatusTemporaryRedirect, "/?customUrl="+c.Request.URL.Path[1:])
184+
})
174185
return nil
175186
}
176187

188+
type interceptResponseWriter struct {
189+
http.ResponseWriter
190+
errH func(http.ResponseWriter, int)
191+
}
192+
193+
func (w *interceptResponseWriter) WriteHeader(status int) {
194+
if status >= http.StatusBadRequest {
195+
w.errH(w.ResponseWriter, status)
196+
w.errH = nil
197+
} else {
198+
w.ResponseWriter.WriteHeader(status)
199+
}
200+
}
201+
202+
type errorHandler func(http.ResponseWriter, int)
203+
204+
func (w *interceptResponseWriter) Write(p []byte) (n int, err error) {
205+
if w.errH == nil {
206+
return len(p), nil
207+
}
208+
return w.ResponseWriter.Write(p)
209+
}
210+
211+
func interceptHandler(next http.Handler, errH errorHandler) http.Handler {
212+
if errH == nil {
213+
errH = customErrorHandler
214+
}
215+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
216+
next.ServeHTTP(&interceptResponseWriter{w, errH}, r)
217+
})
218+
}
219+
220+
func customErrorHandler(w http.ResponseWriter, status int) {
221+
// let 404s fall through: the next NoRoute handler will redirect
222+
// them back to the main page with the customURL box filled out.
223+
if status != 404 {
224+
http.Error(w, "error", status)
225+
}
226+
}
227+
177228
// Listen starts the http server
178229
func (h *Handler) Listen() error {
179230
return h.engine.Run(util.GetConfig().ListenAddr)

internal/handlers/public_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,8 @@ func TestHandleDeletion(t *testing.T) {
275275
t.Fatalf("could not send visit request: %v", err)
276276
}
277277
fmt.Println(body.URL)
278-
if resp.StatusCode != http.StatusNotFound {
279-
t.Fatalf("expected status: %d; got: %d", http.StatusNotFound, resp.StatusCode)
278+
if resp.StatusCode != http.StatusOK {
279+
t.Fatalf("expected status: %d; got: %d", http.StatusOK, resp.StatusCode)
280280
}
281281
}
282282

web/src/Home/Home.js

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,39 @@ import CustomCard from '../Card/Card'
1010
import './Home.css'
1111

1212
export default class HomeComponent extends Component {
13+
constructor(props) {
14+
super(props);
15+
this.urlParams = new URLSearchParams(window.location.search);
16+
this.state = {
17+
links: [],
18+
usedSettings: this.urlParams.get('customUrl') ? ['custom'] : [],
19+
customID: this.urlParams.get('customUrl') ? this.urlParams.get('customUrl') : '',
20+
showCustomIDError: false,
21+
expiration: null
22+
}
23+
}
1324
handleURLChange = (e, { value }) => this.url = value
1425
handlePasswordChange = (e, { value }) => this.password = value
1526
handleCustomExpirationChange = expire => this.setState({ expiration: expire })
1627
handleCustomIDChange = (e, { value }) => {
17-
this.customID = value
28+
this.setState({customID: value})
1829
util.lookupEntry(value, () => this.setState({ showCustomIDError: true }), () => this.setState({ showCustomIDError: false }))
1930
}
20-
onSettingsChange = (e, { value }) => this.setState({ usedSettings: value })
21-
22-
state = {
23-
links: [],
24-
usedSettings: [],
25-
showCustomIDError: false,
26-
expiration: null
31+
onSettingsChange = (e, { value }) => {
32+
this.setState({ usedSettings: value })
2733
}
34+
35+
36+
37+
2838
componentDidMount() {
2939
this.urlInput.focus()
3040
}
3141
handleURLSubmit = () => {
3242
if (!this.state.showCustomIDError) {
3343
util.createEntry({
3444
URL: this.url,
35-
ID: this.customID,
45+
ID: this.state.customID,
3646
Expiration: this.state.usedSettings.includes("expire") && this.state.expiration ? this.state.expiration.toISOString() : undefined,
3747
Password: this.state.usedSettings.includes("protected") && this.password ? this.password : undefined
3848
}, r => this.setState({
@@ -56,13 +66,18 @@ export default class HomeComponent extends Component {
5666
return (
5767
<div>
5868
<Segment raised>
59-
<Header size='huge'>Simplify your links</Header>
69+
{this.urlParams.get("customUrl") && (
70+
<Header size='medium'>I don't have a link named <em>"{this.urlParams.get("customUrl")}"</em> in my database, would
71+
you like to create one?</Header>
72+
) ||
73+
<Header size='huge'>Simplify your links</Header>
74+
}
6075
<Form onSubmit={this.handleURLSubmit} autoComplete="off">
6176
<Form.Field>
6277
<Input required size='large' type='url' ref={input => this.urlInput = input} onChange={this.handleURLChange} placeholder='Paste a link to shorten it' action>
6378
<input />
6479
<MediaQuery query="(min-width: 768px)">
65-
<Select options={options} placeholder='Settings' onChange={this.onSettingsChange} multiple />
80+
<Select options={options} placeholder='Settings' value={this.state.usedSettings} onChange={this.onSettingsChange} multiple />
6681
</MediaQuery>
6782
<Button type='submit'>Shorten<Icon name="arrow right" /></Button>
6883
</Input>
@@ -74,7 +89,7 @@ export default class HomeComponent extends Component {
7489
</MediaQuery>
7590
<Form.Group style={{ marginBottom: "1rem" }}>
7691
{usedSettings.includes("custom") && <Form.Field error={showCustomIDError} width={16}>
77-
<Input label={window.location.origin + "/"} onChange={this.handleCustomIDChange} placeholder='my-shortened-url' />
92+
<Input label={window.location.origin + "/"} onChange={this.handleCustomIDChange} placeholder='my-shortened-url' value={this.state.customID}/>
7893
</Form.Field>}
7994
</Form.Group>
8095
<Form.Group widths="equal">
@@ -100,4 +115,4 @@ export default class HomeComponent extends Component {
100115
</div >
101116
)
102117
}
103-
}
118+
}

0 commit comments

Comments
 (0)