Skip to content

Commit

Permalink
apply cache headers to some most used routes
Browse files Browse the repository at this point in the history
  • Loading branch information
Southclaws committed Jan 12, 2025
1 parent d4cf781 commit 4e7f1a4
Show file tree
Hide file tree
Showing 21 changed files with 696 additions and 579 deletions.
35 changes: 14 additions & 21 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ servers:
security:
- browser: []

x-types:
cache_response_headers: &cache_response_headers
Cache-Control: { $ref: "#/components/headers/Cache-Control" }
Last-Modified: { $ref: "#/components/headers/Last-Modified" }
ETag: { $ref: "#/components/headers/ETag" }

tags:
- name: misc
description: General metadata for the instance and uncategorised routes.
Expand Down Expand Up @@ -636,6 +642,7 @@ paths:
"404": { $ref: "#/components/responses/NotFound" }
"401": { $ref: "#/components/responses/Unauthorised" }
"200": { $ref: "#/components/responses/AccountGetOK" }
"304": { description: cached }

patch:
operationId: AccountUpdate
Expand Down Expand Up @@ -942,6 +949,7 @@ paths:
"404": { $ref: "#/components/responses/NotFound" }
"401": { $ref: "#/components/responses/Unauthorised" }
"200": { $ref: "#/components/responses/ProfileGetOK" }
"304": { description: cached }

/profiles/{account_handle}/followers:
get:
Expand Down Expand Up @@ -992,6 +1000,7 @@ paths:
"404": { $ref: "#/components/responses/NotFound" }
"401": { $ref: "#/components/responses/Unauthorised" }
"200": { $ref: "#/components/responses/ProfileFollowingGetOK" }
"304": { description: cached }

#
# 888 d8b
Expand Down Expand Up @@ -1150,8 +1159,6 @@ paths:
parameters:
- $ref: "#/components/parameters/ThreadMarkParam"
- $ref: "#/components/parameters/PaginationQuery"
- $ref: "#/components/parameters/If-None-Match"
- $ref: "#/components/parameters/If-Modified-Since"
responses:
default: { $ref: "#/components/responses/InternalServerError" }
"404": { $ref: "#/components/responses/NotFound" }
Expand Down Expand Up @@ -1913,21 +1920,6 @@ components:
#

parameters:
"If-None-Match":
description: If-None-Match cache control header.
name: If-None-Match
in: header
required: false
schema:
type: string
"If-Modified-Since":
description: If-Modified-Since cache control header.
name: If-Modified-Since
in: header
required: false
schema:
type: string

IconSize:
description: Icon sizes.
example: "512x512"
Expand Down Expand Up @@ -2653,6 +2645,7 @@ components:

AccountGetOK:
description: OK
headers: { <<: *cache_response_headers }
content:
application/json:
schema:
Expand Down Expand Up @@ -2681,6 +2674,7 @@ components:

AccountGetAvatar:
description: OK
headers: { <<: *cache_response_headers }
content:
image/png:
schema:
Expand Down Expand Up @@ -2731,6 +2725,7 @@ components:

ProfileGetOK:
description: OK
headers: { <<: *cache_response_headers }
content:
application/json:
schema:
Expand Down Expand Up @@ -2807,10 +2802,7 @@ components:

ThreadGet:
description: The information about a thread and its posts.
headers:
Cache-Control: { $ref: "#/components/headers/Cache-Control" }
Last-Modified: { $ref: "#/components/headers/Last-Modified" }
ETag: { $ref: "#/components/headers/ETag" }
headers: { <<: *cache_response_headers }
content:
application/json:
schema: { $ref: "#/components/schemas/Thread" }
Expand Down Expand Up @@ -2845,6 +2837,7 @@ components:

AssetGetOK:
description: The new URL of an uploaded file.
headers: { <<: *cache_response_headers }
content:
"*/*":
schema:
Expand Down
35 changes: 8 additions & 27 deletions app/resources/cachecontrol/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"time"

"github.com/Southclaws/opt"

"github.com/Southclaws/storyden/app/transports/http/openapi"
)

// Query represents a HTTP conditional request query.
Expand All @@ -15,35 +13,22 @@ type Query struct {
}

// NewQuery must be constructed from a HTTP request's conditional headers.
func NewQuery(
IfNoneMatch *string,
IfModifiedSince *string,
) opt.Optional[Query] {
if IfNoneMatch == nil && IfModifiedSince == nil {
return opt.NewEmpty[Query]()
}

modifiedSince, err := opt.MapErr(opt.NewPtr(IfModifiedSince), parseConditionalRequestTime)
if err != nil {
return opt.NewEmpty[Query]()
func NewQuery(ifNoneMatch opt.Optional[string], ifModifiedSince opt.Optional[time.Time]) Query {
return Query{
ETag: ifNoneMatch,
ModifiedSince: ifModifiedSince,
}

return opt.New(Query{
ETag: opt.NewPtr((*string)(IfNoneMatch)),
ModifiedSince: modifiedSince,
})
}

// NotModified takes the current updated date of a resource and returns true if
// the cache control query includes a Is-Modified-Since header and the resource
// updated date is not after the header value. True means a 304 response header.
func (q Query) NotModified(resourceUpdated time.Time) bool {
// truncate the resourceUpdated to the nearest second because the actual
// HTTP header is already truncated but the database time is in nanoseconds.
// If we didn't do this, the resource updated will always be slightly ahead.
truncated := resourceUpdated.Truncate(time.Second)

if ms, ok := q.ModifiedSince.Get(); ok {
// truncate the resourceUpdated to the nearest second because the actual
// HTTP header is already truncated but the DB time is in nanoseconds.
// If we didn't do this the resource time will always be slightly ahead.
truncated := resourceUpdated.Truncate(time.Second)

// If the resource update time is ahead of the HTTP Last-Modified check,
// modified = 1, meaning the resource has been modified since the last
Expand All @@ -55,7 +40,3 @@ func (q Query) NotModified(resourceUpdated time.Time) bool {

return false
}

func parseConditionalRequestTime(in openapi.IfModifiedSince) (time.Time, error) {
return time.Parse(time.RFC1123, in)
}
16 changes: 4 additions & 12 deletions app/resources/post/thread_cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ package thread_cache
import (
"context"

"github.com/Southclaws/fault"
"github.com/Southclaws/fault/fctx"
"github.com/Southclaws/opt"
"github.com/rs/xid"

"github.com/Southclaws/storyden/app/resources/cachecontrol"
Expand All @@ -23,18 +20,13 @@ func New(db *ent.Client) *Cache {
}
}

func (c *Cache) IsNotModified(ctx context.Context, cq opt.Optional[cachecontrol.Query], id xid.ID) (bool, error) {
query, ok := cq.Get()
if !ok {
return false, nil
}

func (c *Cache) IsNotModified(ctx context.Context, cq cachecontrol.Query, id xid.ID) bool {
r, err := c.db.Post.Query().Select(post.FieldUpdatedAt).Where(post.ID(id)).Only(ctx)
if err != nil {
return false, fault.Wrap(err, fctx.With(ctx))
return false
}

notModified := query.NotModified(r.UpdatedAt)
notModified := cq.NotModified(r.UpdatedAt)

return notModified, nil
return notModified
}
5 changes: 4 additions & 1 deletion app/resources/profile/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
type Public struct {
ID account.AccountID
Created time.Time
Updated time.Time
Deleted opt.Optional[time.Time]

Handle string
Expand All @@ -45,7 +46,7 @@ func (p *Public) GetContent() datagraph.Content { return p.Bio }
func (p *Public) GetProps() map[string]any { return p.Metadata }
func (p *Public) GetAssets() []*asset.Asset { return []*asset.Asset{} }
func (p *Public) GetCreated() time.Time { return p.Created }
func (p *Public) GetUpdated() time.Time { return p.Created }
func (p *Public) GetUpdated() time.Time { return p.Updated }

func ProfileFromModel(a *ent.Account) (*Public, error) {
rolesEdge := a.Edges.AccountRoles
Expand Down Expand Up @@ -84,6 +85,7 @@ func ProfileFromModel(a *ent.Account) (*Public, error) {
return &Public{
ID: account.AccountID(a.ID),
Created: a.CreatedAt,
Updated: a.UpdatedAt,
Deleted: opt.NewPtr(a.DeletedAt),
Handle: a.Handle,
Name: a.Name,
Expand All @@ -99,6 +101,7 @@ func ProfileFromAccount(a *account.Account) *Public {
return &Public{
ID: a.ID,
Created: a.CreatedAt,
Updated: a.UpdatedAt,
Deleted: a.DeletedAt,
Handle: a.Handle,
Name: a.Name,
Expand Down
33 changes: 33 additions & 0 deletions app/resources/profile/profile_cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package profile_cache

import (
"context"

"github.com/rs/xid"

"github.com/Southclaws/storyden/app/resources/cachecontrol"
"github.com/Southclaws/storyden/internal/ent"
"github.com/Southclaws/storyden/internal/ent/account"
"github.com/Southclaws/storyden/internal/ent/post"
)

type Cache struct {
db *ent.Client
}

func New(db *ent.Client) *Cache {
return &Cache{
db: db,
}
}

func (c *Cache) IsNotModified(ctx context.Context, cq cachecontrol.Query, id xid.ID) bool {
r, err := c.db.Account.Query().Select(post.FieldUpdatedAt).Where(account.ID(id)).Only(ctx)
if err != nil {
return false
}

notModified := cq.NotModified(r.UpdatedAt)

return notModified
}
2 changes: 2 additions & 0 deletions app/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"github.com/Southclaws/storyden/app/resources/post/thread_cache"
"github.com/Southclaws/storyden/app/resources/profile/follow_querier"
"github.com/Southclaws/storyden/app/resources/profile/follow_writer"
"github.com/Southclaws/storyden/app/resources/profile/profile_cache"
"github.com/Southclaws/storyden/app/resources/profile/profile_search"
"github.com/Southclaws/storyden/app/resources/question"
"github.com/Southclaws/storyden/app/resources/settings"
Expand Down Expand Up @@ -90,6 +91,7 @@ func Build() fx.Option {
link_querier.New,
link_writer.New,
profile_search.New,
profile_cache.New,
follow_writer.New,
follow_querier.New,
event_querier.New,
Expand Down
6 changes: 3 additions & 3 deletions app/services/authentication/provider/webauthn/webauthn.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/Southclaws/storyden/app/resources/account/account_writer"
"github.com/Southclaws/storyden/app/resources/account/authentication"
"github.com/Southclaws/storyden/app/services/account/register"
"github.com/Southclaws/storyden/app/transports/http/middleware/useragent"
"github.com/Southclaws/storyden/app/services/reqinfo"
)

var (
Expand Down Expand Up @@ -109,7 +109,7 @@ func (p *Provider) register(ctx context.Context, handle string, credential *weba
base64.RawURLEncoding.EncodeToString(credential.ID),
string(encoded),
nil,
authentication.WithName(useragent.GetDeviceName(ctx)),
authentication.WithName(reqinfo.GetDeviceName(ctx)),
)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
Expand All @@ -136,7 +136,7 @@ func (p *Provider) add(ctx context.Context, accountID account.AccountID, credent
base64.RawURLEncoding.EncodeToString(credential.ID),
string(encoded),
nil,
authentication.WithName(useragent.GetDeviceName(ctx)),
authentication.WithName(reqinfo.GetDeviceName(ctx)),
)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
Expand Down
71 changes: 71 additions & 0 deletions app/services/reqinfo/info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package reqinfo

import (
"context"
"fmt"
"net/http"
"time"

"github.com/mileusna/useragent"

"github.com/Southclaws/opt"
"github.com/Southclaws/storyden/app/resources/cachecontrol"
)

type Info struct {
UserAgent useragent.UserAgent
CacheQuery cachecontrol.Query
}

type infoKey struct{}

func WithRequestInfo(ctx context.Context, r *http.Request) context.Context {
ua := useragent.Parse(r.Header.Get("User-Agent"))

ifNoneMatch := opt.NewIf(r.Header.Get("If-None-Match"), notEmpty)

ifModifiedSince, err := opt.MapErr(
opt.NewIf(r.Header.Get("If-Modified-Since"), notEmpty),
parseConditionalRequestTime,
)
if err != nil {
ifModifiedSince = opt.NewEmpty[time.Time]()
}

info := Info{
UserAgent: ua,
CacheQuery: cachecontrol.NewQuery(ifNoneMatch, ifModifiedSince),
}

return context.WithValue(ctx, infoKey{}, info)
}

func GetDeviceName(ctx context.Context) string {
v := ctx.Value(infoKey{})
i, ok := v.(Info)
if !ok {
return "Unknown"
}

ua := i.UserAgent

return fmt.Sprintf("%s (%s)", ua.Name, ua.OS)
}

func GetCacheQuery(ctx context.Context) cachecontrol.Query {
v := ctx.Value(infoKey{})
i, ok := v.(Info)
if !ok {
return cachecontrol.Query{}
}

return i.CacheQuery
}

func notEmpty(s string) bool {
return s != ""
}

func parseConditionalRequestTime(in string) (time.Time, error) {
return time.Parse(time.RFC1123, in)
}
Loading

0 comments on commit 4e7f1a4

Please sign in to comment.