Search code examples
postgresqlgojson-apigo-httpgo-chi

Post Request with PostgreSQL and json-api returns an empty body


After a POST request, i was expecting to have a last inserted record marshalled into json, but instead returns an empty body. What am i not doing well?

package models


import (
    "encoding/json"
    "errors"
    "flag"
    "fmt"
    "log"
    "net/http"
    "strconv"

    "github.com/go-chi/chi"
    "github.com/google/jsonapi"
    "github.com/thedevsaddam/renderer"
    "github.com/xo/dburl"
)

var rnd = renderer.New()

var flagVerbose = flag.Bool("v", false, "verbose")

var FlagURL = flag.String("url", "postgres://postgres:@127.0.0.1/sweb", "url")

// Page represents a row from 'public.pages'.
type Page struct {
    Tag   string `jsonapi:"attr,tag"`      // tag
    Body  string `jsonapi:"attr,body"`     // body
    Slug  string `jsonapi:"attr,slug"`     // slug
    Title string `jsonapi:"attr,title"`    // title
    ID    int    `jsonapi:"primary,pages"` // id
    Link  string `jsonapi:"attr,link"`     // link

    // xo fields
    _exists, _deleted bool
}

func (page Page) JSONAPILinks() *jsonapi.Links {
    return &jsonapi.Links{
        "self": fmt.Sprintf("https://%d", page.ID),
    }
}

I believe this is the culprit. After inserting a record, it should return the last inserted record as specified.

func (p *Page) PInsert(db XODB) (*Page, error) {
    var err error

    // if already exist, bail
    if p._exists {
        return p, errors.New("insert failed: already exists")
    }

    // sql insert query, primary key provided by sequence
    const sqlstr = `INSERT INTO public.pages (` +
        `tag, body, slug, title` +
        `) VALUES (` +
        `$1, $2, $3, $4` +
        `) RETURNING id, tag, body, title`

    // run query
    XOLog(sqlstr, p.Tag, p.Body, p.Slug, p.Title)
    err = db.QueryRow(sqlstr, p.Tag, p.Body, p.Slug, p.Title).Scan(&p.ID, &p.Tag, &p.Body, &p.Title)
    if err != nil {
        return p, err
    }

    // set existence
    p._exists = true

    return p, nil
}

Update updates the Page in the database and return last inserted records. The same should apply for the Update function

func (p *Page) Update(db XODB) (*Page, error) {
    var err error

    // if doesn't exist, bail
    if !p._exists {
        return p, errors.New("update failed: does not exist")
    }

    // if deleted, bail
    if p._deleted {
        return p, errors.New("update failed: marked for deletion")
    }

    // sql query
    const sqlstr = `UPDATE public.pages SET (` +
        `tag, body, slug, title` +
        `) = ( ` +
        `$1, $2, $3, $4` +
        `) WHERE id = $5`

    // run query
    XOLog(sqlstr, p.Tag, p.Body, p.Slug, p.Title, p.ID)
    _, err = db.Exec(sqlstr, p.Tag, p.Body, p.Slug, p.Title, p.ID)
    return p, err
}

func (p *Page) PSave(db XODB) (*Page, error) {
    if p.Exists() {
        return p.Update(db)
    }

    return p.PInsert(db)
}


func NewPage(w http.ResponseWriter, r *http.Request) {

    db, err := dburl.Open(*FlagURL)
    defer db.Close()
    if err != nil {
        log.Fatal(err)
    }

    var page Page

    //page := new(Page)

    if err := jsonapi.UnmarshalPayload(r.Body, &page); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }


    p, err := page.PSave(db)
    if err != nil {
        fmt.Println(err)
        if err := jsonapi.MarshalPayload(w, p); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            fmt.Println(err)
        }


    }


    w.Header().Set("Content-Type", jsonapi.MediaType)
    w.WriteHeader(http.StatusCreated)

}

This is the last function i believe the issue is happening from. the last inserted record supposed to be marshalled into json.


Solution

  • Your last section of code contains a number of mistakes. The relevant section (without the useless and obfuscating Printlns) is:

    p, err := page.PSave(db)
    if err != nil {
        if err := jsonapi.MarshalPayload(w, p); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
    }
    
    w.Header().Set("Content-Type", jsonapi.MediaType)
    w.WriteHeader(http.StatusCreated)
    

    And the primary mistake is that json.MarshalPayload is only called when err != nil. In other words, you only serialize the page if you failed to save it.

    The secondary mistake is that jsonapi.MarshalPayload will call Write on the http.ResponseWriter. This turns all subsequent calls to Header().Set and WriteHeader into no-ops.

    More correct code would look like this.

    // 1. Save the page in the database, bail on error
    p, err := page.PSave(db)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return 
    }
    
    // 2. Marshal the page into an intermediate buffer, bail on error
    var buf bytes.Buffer
    if err := jsonapi.MarshalPayload(&buf, p); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return 
    }
    
    // 3. Write the entire response; failures to write the intermediate buffer
    // cannot be communicated over HTTP
    w.Header().Set("Content-Type", jsonapi.MediaType)
    w.WriteHeader(http.StatusCreated)
    if _, err := buf.WriteTo(w); err != nil {
        log.Printf("failed to write response: %v", err)
        return 
    }