Skip to content

Commit dedac80

Browse files
committed
Get rid of github.com/go-chi/render use
Replace go-chi/render with go-pkgz/rest for JSON responses and custom helpers for HTML/plain text responses. Key changes: - Replace render.JSON/render.Status with rest.RenderJSON and explicit w.WriteHeader() calls - Replace render.DecodeJSON with json.NewDecoder().Decode() - Add SendErrorJSON helper that sets Content-Type header before WriteHeader (required since rest.RenderJSON can't set headers after WriteHeader is called) - Add HTMLResponse and PlainTextResponse helpers Fix export double-execution in migrator.go: The original code called Export twice - once to io.Discard to check for errors, then again to actually write. This was wasteful and had a race condition risk. Now file mode buffers to memory first for atomic success/failure, while stream mode writes directly with proper error handling.
1 parent 34ea4c3 commit dedac80

File tree

8 files changed

+91
-78
lines changed

8 files changed

+91
-78
lines changed

backend/app/rest/api/admin.go

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"time"
88

99
"github.com/go-chi/chi/v5"
10-
"github.com/go-chi/render"
1110
"github.com/go-pkgz/auth/v2"
1211
cache "github.com/go-pkgz/lcw/v2"
1312
log "github.com/go-pkgz/lgr"
@@ -54,8 +53,7 @@ func (a *admin) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) {
5453
return
5554
}
5655
a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.SiteID, locator.URL, lastCommentsScope))
57-
render.Status(r, http.StatusOK)
58-
render.JSON(w, r, R.JSON{"id": id, "locator": locator})
56+
R.RenderJSON(w, R.JSON{"id": id, "locator": locator})
5957
}
6058

6159
// DELETE /user/{userid}?site=side-id - delete all user comments for requested userid
@@ -69,8 +67,7 @@ func (a *admin) deleteUserCtrl(w http.ResponseWriter, r *http.Request) {
6967
return
7068
}
7169
a.cache.Flush(cache.Flusher(siteID).Scopes(userID, siteID, lastCommentsScope))
72-
render.Status(r, http.StatusOK)
73-
render.JSON(w, r, R.JSON{"user_id": userID, "site_id": siteID})
70+
R.RenderJSON(w, R.JSON{"user_id": userID, "site_id": siteID})
7471
}
7572

7673
// GET /user/{userid}?site=side-id - get user info for requested userid
@@ -84,8 +81,7 @@ func (a *admin) getUserInfoCtrl(w http.ResponseWriter, r *http.Request) {
8481
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get user info", rest.ErrInternal)
8582
return
8683
}
87-
render.Status(r, http.StatusOK)
88-
render.JSON(w, r, ucomments[0].User)
84+
R.RenderJSON(w, ucomments[0].User)
8985
}
9086

9187
// GET /deleteme?token=jwt - delete all user comments and details by user's request. Gets info about deleted used from provided token
@@ -135,8 +131,7 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) {
135131
}
136132

137133
a.cache.Flush(cache.Flusher(audience).Scopes(audience, claims.User.ID, lastCommentsScope))
138-
render.Status(r, http.StatusOK)
139-
render.JSON(w, r, R.JSON{"user_id": claims.User.ID, "site_id": claims.Audience})
134+
R.RenderJSON(w, R.JSON{"user_id": claims.User.ID, "site_id": claims.Audience})
140135
}
141136

142137
// PUT /user/{userid}?site=side-id&block=1&ttl=7d - block or unblock user
@@ -164,7 +159,7 @@ func (a *admin) setBlockCtrl(w http.ResponseWriter, r *http.Request) {
164159
}
165160
}
166161
a.cache.Flush(cache.Flusher(siteID).Scopes(userID, siteID, lastCommentsScope))
167-
render.JSON(w, r, R.JSON{"user_id": userID, "site_id": siteID, "block": blockStatus})
162+
R.RenderJSON(w, R.JSON{"user_id": userID, "site_id": siteID, "block": blockStatus})
168163
}
169164

170165
// GET /blocked?site=siteID - list blocked users
@@ -175,7 +170,7 @@ func (a *admin) blockedUsersCtrl(w http.ResponseWriter, r *http.Request) {
175170
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get blocked users", rest.ErrSiteNotFound)
176171
return
177172
}
178-
render.JSON(w, r, users)
173+
R.RenderJSON(w, users)
179174
}
180175

181176
// PUT /readonly?site=siteID&url=post-url&ro=1 - set or reset read-only status for the post
@@ -202,7 +197,7 @@ func (a *admin) setReadOnlyCtrl(w http.ResponseWriter, r *http.Request) {
202197
return
203198
}
204199
a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, locator.SiteID))
205-
render.JSON(w, r, R.JSON{"locator": locator, "read-only": roStatus})
200+
R.RenderJSON(w, R.JSON{"locator": locator, "read-only": roStatus})
206201
}
207202

208203
// PUT /title/{id}?site=siteID&url=post-url - set comment PostTitle to page's title
@@ -218,8 +213,7 @@ func (a *admin) setTitleCtrl(w http.ResponseWriter, r *http.Request) {
218213
log.Printf("[INFO] set comment's title %s to %q", id, c.PostTitle)
219214

220215
a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, lastCommentsScope))
221-
render.Status(r, http.StatusOK)
222-
render.JSON(w, r, R.JSON{"id": id, "locator": locator})
216+
R.RenderJSON(w, R.JSON{"id": id, "locator": locator})
223217
}
224218

225219
// PUT /verify?site=siteID&url=post-url&ro=1 - set or reset read-only status for the post
@@ -233,7 +227,7 @@ func (a *admin) setVerifyCtrl(w http.ResponseWriter, r *http.Request) {
233227
return
234228
}
235229
a.cache.Flush(cache.Flusher(siteID).Scopes(siteID, userID))
236-
render.JSON(w, r, R.JSON{"user": userID, "verified": verifyStatus})
230+
R.RenderJSON(w, R.JSON{"user": userID, "verified": verifyStatus})
237231
}
238232

239233
// PUT /pin/{id}?site=siteID&url=post-url&pin=1
@@ -248,5 +242,5 @@ func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) {
248242
return
249243
}
250244
a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL))
251-
render.JSON(w, r, R.JSON{"id": commentID, "locator": locator, "pin": pinStatus})
245+
R.RenderJSON(w, R.JSON{"id": commentID, "locator": locator, "pin": pinStatus})
252246
}

backend/app/rest/api/migrator.go

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package api
22

33
import (
4+
"bytes"
45
"compress/gzip"
56
"context"
67
"fmt"
78
"io"
89
"net/http"
910
"os"
11+
"strconv"
1012
"sync"
1113
"time"
1214

13-
"github.com/go-chi/render"
1415
cache "github.com/go-pkgz/lcw/v2"
1516
log "github.com/go-pkgz/lgr"
1617
R "github.com/go-pkgz/rest"
@@ -58,8 +59,8 @@ func (m *Migrator) importCtrl(w http.ResponseWriter, r *http.Request) {
5859

5960
go m.runImport(siteID, r.URL.Query().Get("provider"), tmpfile) // import runs in background and sets busy flag for site
6061

61-
render.Status(r, http.StatusAccepted)
62-
render.JSON(w, r, R.JSON{"status": "import request accepted"})
62+
w.WriteHeader(http.StatusAccepted)
63+
R.RenderJSON(w, R.JSON{"status": "import request accepted"})
6364
}
6465

6566
// POST /import/form?secret=key&site=site-id&provider=disqus|remark|wordpress
@@ -93,8 +94,8 @@ func (m *Migrator) importFormCtrl(w http.ResponseWriter, r *http.Request) {
9394

9495
go m.runImport(siteID, r.URL.Query().Get("provider"), tmpfile) // import runs in background and sets busy flag for site
9596

96-
render.Status(r, http.StatusAccepted)
97-
render.JSON(w, r, R.JSON{"status": "import request accepted"})
97+
w.WriteHeader(http.StatusAccepted)
98+
R.RenderJSON(w, R.JSON{"status": "import request accepted"})
9899
}
99100

100101
// GET /wait?site=site-id
@@ -114,38 +115,46 @@ func (m *Migrator) waitCtrl(w http.ResponseWriter, r *http.Request) {
114115

115116
select {
116117
case <-ctx.Done():
117-
render.Status(r, http.StatusGatewayTimeout)
118-
render.JSON(w, r, R.JSON{"status": "timeout expired", "site_id": siteID})
118+
w.WriteHeader(http.StatusGatewayTimeout)
119+
R.RenderJSON(w, R.JSON{"status": "timeout expired", "site_id": siteID})
119120
return
120121
case <-time.After(100 * time.Millisecond):
121122
}
122123
}
123-
render.Status(r, http.StatusOK)
124-
render.JSON(w, r, R.JSON{"status": "completed", "site_id": siteID})
124+
R.RenderJSON(w, R.JSON{"status": "completed", "site_id": siteID})
125125
}
126126

127127
// GET /export?site=site-id&secret=12345&?mode=file|stream
128128
// exports all comments for siteID as gz file
129129
func (m *Migrator) exportCtrl(w http.ResponseWriter, r *http.Request) {
130130
siteID := r.URL.Query().Get("site")
131131

132-
var writer io.Writer = w
133132
if r.URL.Query().Get("mode") == "file" {
133+
// buffer to memory to handle errors before committing to response
134+
var buf bytes.Buffer
135+
gzWriter := gzip.NewWriter(&buf)
136+
if _, err := m.NativeExporter.Export(gzWriter, siteID); err != nil {
137+
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed", rest.ErrInternal)
138+
return
139+
}
140+
if err := gzWriter.Close(); err != nil {
141+
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed", rest.ErrInternal)
142+
return
143+
}
144+
134145
exportFile := fmt.Sprintf("%s-%s.json.gz", siteID, time.Now().Format("20060102"))
135146
w.Header().Set("Content-Type", "application/gzip")
136147
w.Header().Set("Content-Disposition", "attachment;filename="+exportFile)
137-
gzWriter := gzip.NewWriter(w)
138-
defer func() {
139-
if e := gzWriter.Close(); e != nil {
140-
log.Printf("[WARN] can't close gzip writer, %s", e)
141-
}
142-
}()
143-
writer = gzWriter
148+
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
149+
if _, err := io.Copy(w, &buf); err != nil {
150+
log.Printf("[WARN] failed to write export response: %v", err)
151+
}
152+
return
144153
}
145154

146-
if _, err := m.NativeExporter.Export(writer, siteID); err != nil {
155+
// stream mode - write directly to response
156+
if _, err := m.NativeExporter.Export(w, siteID); err != nil {
147157
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed", rest.ErrInternal)
148-
return
149158
}
150159
}
151160

@@ -201,8 +210,8 @@ func (m *Migrator) remapCtrl(w http.ResponseWriter, r *http.Request) {
201210
log.Printf("[DEBUG] convert request completed. site=%s, comments=%d", siteID, size)
202211
}()
203212

204-
render.Status(r, http.StatusAccepted)
205-
render.JSON(w, r, R.JSON{"status": "convert request accepted"})
213+
w.WriteHeader(http.StatusAccepted)
214+
R.RenderJSON(w, R.JSON{"status": "convert request accepted"})
206215
}
207216

208217
// runImport reads from tmpfile and import for given siteID and provider

backend/app/rest/api/rest.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"github.com/go-chi/chi/v5"
2121
"github.com/go-chi/chi/v5/middleware"
2222
"github.com/go-chi/cors"
23-
"github.com/go-chi/render"
2423
"github.com/go-pkgz/auth/v2"
2524
"github.com/go-pkgz/lcw/v2"
2625
log "github.com/go-pkgz/lgr"
@@ -473,8 +472,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
473472
if cnf.Admins == nil { // prevent json serialization to nil
474473
cnf.Admins = []string{}
475474
}
476-
render.Status(r, http.StatusOK)
477-
render.JSON(w, r, cnf)
475+
R.RenderJSON(w, cnf)
478476
}
479477

480478
// serves static files from the webRoot directory or files embedded into the compiled binary if that directory is absent

0 commit comments

Comments
 (0)