0

I am writing an HTTP server in Go that interacts with a MongoDB database. The GET, POST and PUT routes all work perfectly as expected - PUT is the most similar to DELETE in its content and works - whereas DELETE doesn't.

My delete route when searching for the document will return mongo.ErrNoDocuments even though the UUID definitely exists and the other routes (PUT and GET) work perfectly with the same UUID.

I am still new to Go so I am sure I am missing something. Any help is greatly appreciated

Here is the code:

DELETE route

/* DELETE /{uuid}
r.Body:
    "accessKey"  -> required

Deletes an existing Paste in the MongoDB database
*/
func (s *Server) deletePaste() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        defer func() {
            log.Printf("%s %s [%v]\n",
                r.Method,
                r.URL.Path,
                time.Since(start),
            )
        }()

        uuidStr, _ := mux.Vars(r)["uuid"]
        fmt.Println(uuidStr)

        body := struct {
            AccessKey string `json:"accessKey,omitempty"`
        }{}
        if err := decodeJSONBody(w, r, &body); err != nil {
            var mr *badRequest
            if errors.As(err, &mr) {
                http.Error(w, mr.msg, mr.status)
            } else {
                log.Print(err.Error())
                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            }
            return
        }

        coll := s.Client.Database("paste").Collection("files")
        // Check document exists and accessKey is the same
        var result bson.M
        filter := bson.M{"uuid": uuidStr}
        project := bson.M{
            "_id":       0,
            "accessKey": 1,
        }

        err := coll.FindOne(
            context.TODO(),
            filter,
            options.FindOne().SetProjection(project),
        ).Decode(&result)

        if err != nil {
            if err == mongo.ErrNoDocuments {
                http.Error(w, "No document found with that UUID", http.StatusBadRequest)
                return
            }
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // Check the sender can actually edit the paste
        if body.AccessKey != result["accessKey"] {
            http.Error(w, "Invalid access key", http.StatusUnauthorized)
            return
        }

        // Delete matching document
        //opts := options.Delete().SetHint(bson.D{{Key: "uuid", Value: 1}})
        res, err := coll.DeleteOne(context.TODO(), filter, nil)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        if res.DeletedCount == 0 {
            http.Error(w, "Error matching and deleting document", http.StatusInternalServerError)
            return
        }

        response := make(map[string]string)
        response["info"] = "Document deleted"

        w.Header().Set("Content-Type", "application/json")
        if err := json.NewEncoder(w).Encode(response); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    }
}

PUT route

/* PUT /{uuid}
r.Body:
    "accessKey"  -> required
    "content"     -> optional
    "name"        -> optional
    "filetype"    -> optional
    "expiresIn"   -> optional
    ^ At least one of the 4 optional fields must be updated

Updates an existing Paste in the MongoDB database and returns a JSON document
{
    uuid:       UUID,
    name:       String,
    content:    String,
    filetype:   String,
    accessKey:  String,
    expiresAt:  Date
}
*/
func (s *Server) updatePaste() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        defer func() {
            log.Printf("%s %s [%v]\n",
                r.Method,
                r.URL.Path,
                time.Since(start),
            )
        }()

        uuidStr, _ := mux.Vars(r)["uuid"]

        var paste Paste
        var body PasteBody
        if err := decodeJSONBody(w, r, &body); err != nil {
            var mr *badRequest
            if errors.As(err, &mr) {
                http.Error(w, mr.msg, mr.status)
            } else {
                log.Print(err.Error())
                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            }
            return
        }

        if err := paste.EditPaste(&body); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }

        doc, err := toBsonDoc(&paste)
        if err != nil {
            log.Println(err)
            http.Error(w, "Error converting request body to BSON document", http.StatusInternalServerError)
        }

        coll := s.Client.Database("pastes").Collection("files")
        // Check document and provided accessKey match
        var result bson.M
        filter := bson.M{"uuid": uuidStr}
        project := bson.M{
            "_id":       0,
            "accessKey": 1,
        }

        err = coll.FindOne(
            context.TODO(),
            filter,
            options.FindOne().SetProjection(project),
        ).Decode(&result)

        if err != nil {
            if err == mongo.ErrNoDocuments {
                http.Error(w, "No document found with that UUID", http.StatusBadRequest)
                return
            }
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // Check the sender can actually edit the paste
        if paste.AccessKey != result["accessKey"] {
            http.Error(w, "Invalid access key", http.StatusUnauthorized)
            return
        }

        // Update document
        filter = bson.M{"uuid": uuidStr}
        update := bson.M{"$set": doc}
        res, err := coll.UpdateOne(context.TODO(), filter, update)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        if res.MatchedCount == 0 || res.ModifiedCount == 0 {
            http.Error(w, "Error matching and updating document", http.StatusInternalServerError)
            return
        }

        response := make(map[string]string)
        response["uuid"] = uuidStr
        response["expiresAt"] = paste.ExpiresAt.Time().String()

        w.Header().Set("Content-Type", "application/json")
        if err := json.NewEncoder(w).Encode(response); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    }
}

GET route

/* GET /{uuid}

Returns the Paste from the MongoDB database with the matching UUID in JSON
{
    uuid:       UUID,
    name:       String,
    content:    String,
    filetype:   String,
    accessKey:  String,
    expires:    Date
}
*/
func (s *Server) getPaste() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        defer func() {
            log.Printf("%s %s [%v]\n",
                r.Method,
                r.URL.Path,
                time.Since(start),
            )
        }()

        uuidStr, _ := mux.Vars(r)["uuid"]

        coll := s.Client.Database("pastes").Collection("files")
        var result bson.M
        filter := bson.M{"uuid": uuidStr}
        project := bson.M{
            "_id":       0,
            "accessKey": 0,
            "uuid":      0,
        }

        err := coll.FindOne(
            context.TODO(),
            filter,
            options.FindOne().SetProjection(project),
        ).Decode(&result)

        if err != nil {
            if err == mongo.ErrNoDocuments {
                http.Error(w, "No document found with that UUID", http.StatusBadRequest)
                return
            }
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // Convert to long date format
        result["expiresAt"] = primitive.DateTime(result["expiresAt"].(primitive.DateTime)).Time().String()

        w.Header().Set("Content-Type", "application/json")
        if err := json.NewEncoder(w).Encode(result); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    }
}

EDIT:

@icza spotted my typo in using "paste" not "pastes" as the database name when finding the collection. Implementing global variables to hold these strings solved this from happening again.

h5law
  • 1
  • 4
  • In your Delete handler `deletePaste()` you mistyped the collection name: `"paste"`, in all other handlers you use `"pastes"`. This is a very good example why you should use constants for reoccuring values: either all will fail and you'll know exactly why, or all will work and you save yourself this debugging time. – icza Aug 10 '22 at 17:25
  • So be sure to use a package level variable or constant `const collPastes = "pastes"` and use it wherever you intend to refer to this collection, e.g. `s.Client.Database(collPastes)` – icza Aug 10 '22 at 17:25
  • Note that MongoDB is schema-less: databases, collections, properties do not need to exist, they are created on demand. This also means you don't get an error if you mistype their names. See [How to create/drop mongoDB database and collections from a golang (go language) program.?](https://stackoverflow.com/questions/70904594/how-to-create-drop-mongodb-database-and-collections-from-a-golang-go-language/70906294#70906294) – icza Aug 10 '22 at 17:27
  • @icza I can't believe it was so obvious! Thank you so much I feel like just staring at this code for ages has made me blind to that typo. I've implemented global variables for db name and collection name so this doesn't happen again – h5law Aug 10 '22 at 18:12

0 Answers0