You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by mi...@apache.org on 2021/03/11 01:52:22 UTC

[trafficcontrol] branch master updated: Non-"CRUD-er" DSRs (#5489)

This is an automated email from the ASF dual-hosted git repository.

mitchell852 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 41669a1  Non-"CRUD-er" DSRs (#5489)
41669a1 is described below

commit 41669a17927a1d11e8e7f5cbbf930bec2ac216d1
Author: ocket8888 <oc...@apache.org>
AuthorDate: Wed Mar 10 18:52:07 2021 -0700

    Non-"CRUD-er" DSRs (#5489)
    
    * Add constant for Last-Modified header date/time format
    
    * Add api function for writing an IMS 'hit' response
    
    * Add APIInfo method for checking if IMS should be used, and one for creating a Changelog entry
    
    * Make shared_handlers use library constant format
    
    * Add construction method for 'TimeNoMod's from 'time.Time's
    
    * Add new DSR version and associated methods
    
    * Add db helper to check for existing, open DSR with a given XMLID
    
    * Add convenience method for setting DSR's XMLID
    
    * Add GET method for DSR assignments, reworked PUT to not use CRUDer
    
    * Fix incorrect major API version check
    
    * Add example test for DSR.SetXMLID
    
    * Add GET handler for DSR status route, rework PUT to not use the CRUDer
    
    * Rework DSRs to not use the CRUDer
    
    * Fix broken routes referencing old CRUD-er DSR methods
    
    * Add docs for GET method of DSR assignee route, update docs for PUT method
    
    * Add plural terms for 'DSR' to the glossary
    
    * Add docs for GET method of DSR status route, update docs for PUT method
    
    * Updated changelog
    
    * Updated Go client DSR methods
    
    * Update APIv4 DSR tests
    
    * Fix printing pointers instead of the values to which they point
    
    * Fix malformed table
    
    * Fix another malformed table
    
    * Fix tests checking for exact alert content
    
    * Add convenience method to APIInfo to tell if If-Unmodified-Since should be used
    
    * Make GoDocs complete sentences
    
    * Make PUT requests to deliveryservice_requests respect If-Unmodified-Since
    
    * Add client method that will decode a response without swallowing alerts in the event of an error
    
    * Make DSR POST client method use non-swallowing request method
    
    * Fix IUS usage in DSRs respecting 'UseIMS' setting - it shouldn't, apparently
    
    * Fix bad query in IUS handling in DSRs PUT handler
    
    * Remove checking for exact wording in error message
    
    * Add error constants to the API package
    
    * Add request reference to APIInfos
    
    * Replace some constant string usage with constant errors
    
    * Replace 'UseIUS' with a catch-all method for preconditions, update usage
    
    * Fix printing pointer values instead of the values to which they point
    
    * Fix segfault in DSRs integration v4 test
    
    * Fix DSRs missing required fields in fixture data
    
    * Fix DSRs POST allowing duplicate active DSRs for an XMLID in version 4
    
    * Fix DSRs POST client method swallowing alerts in the event of an error
    
    * Fix DSR test checking for exact error wording
    
    * Fix APIv4 tests permanently altering DSRs on creation, causing subsequent failures when IDs change
    
    * Reconcile cacheURL changes in rebase
    
    * Fix not passing DSR by reference to ozzo validations
    
    * Fix compilation error in v3-client
    
    * 'Fix' failures in dsr client API v3/v4 tests
    
    * Add handling for when a DS on PUT doesn't exist
    
    * Fix segfaults in PUT handler, fix other cases where non-existent DSR causes ISE
    
    * Fix DSR API methods pre-parsing bodies - toclientlib now handles that
    
    * Fix docs typo
    
    * Fix not being able to modify DSR status through dedicated endpoint
    
    * changes payload of changing assignee and status of dsrs
    
    (cherry picked from commit eb489a85fd32aa8edb70a0a62cecbaec0a4d4e56)
    
    * Fix DSR XMLID not appearing in changelog messages
    
    * Add missing 'created_at' query parameter
    
    * Add missing documentation for sorting and pagination query parameters
    
    Co-authored-by: Jeremy Mitchell <mi...@gmail.com>
---
 CHANGELOG.md                                       |   3 +
 docs/source/api/v2/deliveryservice_requests.rst    |  68 +-
 docs/source/api/v3/deliveryservice_requests.rst    |  68 +-
 docs/source/api/v4/deliveryservice_requests.rst    |  68 +-
 .../api/v4/deliveryservice_requests_id_assign.rst  |  90 +-
 .../api/v4/deliveryservice_requests_id_status.rst  |  81 +-
 docs/source/glossary.rst                           |   2 +
 lib/go-rfc/cachecontrol.go                         |   5 +-
 lib/go-rfc/http.go                                 |   4 +
 lib/go-tc/deliveryservice_requests.go              | 366 +++++++-
 lib/go-tc/deliveryservice_requests_test.go         | 118 +++
 lib/go-tc/time.go                                  |   9 +-
 .../api/v1/deliveryservice_requests_test.go        |  73 +-
 .../api/v2/deliveryservice_requests_test.go        |  75 +-
 .../api/v3/deliveryservice_requests_test.go        |  69 +-
 .../v4/deliveryservice_request_comments_test.go    |  31 +-
 .../api/v4/deliveryservice_requests_test.go        | 242 ++++--
 traffic_ops/testing/api/v4/tc-fixtures.json        |  33 +-
 traffic_ops/testing/api/v4/traffic_control_test.go |   2 +-
 traffic_ops/traffic_ops_golang/api/api.go          | 114 ++-
 traffic_ops/traffic_ops_golang/api/generic_crud.go |   2 +-
 .../traffic_ops_golang/api/shared_handlers.go      |   3 +-
 .../traffic_ops_golang/dbhelpers/db_helpers.go     |  24 +-
 .../deliveryservice/request/assign.go              | 216 +++++
 .../deliveryservice/request/requests.go            | 924 ++++++++++++++-------
 .../deliveryservice/request/requests_test.go       | 226 ++---
 .../deliveryservice/request/status.go              | 180 ++++
 .../deliveryservice/request/validate.go            |  98 ++-
 traffic_ops/traffic_ops_golang/routing/routes.go   |  52 +-
 traffic_ops/traffic_ops_golang/util/ims/ims.go     |   5 +-
 traffic_ops/v3-client/session.go                   |  95 ++-
 traffic_ops/v4-client/deliveryservice_requests.go  | 158 ++--
 .../common/api/DeliveryServiceRequestService.js    |   6 +-
 .../edit/FormEditDeliveryServiceController.js      |   4 +-
 .../new/FormNewDeliveryServiceController.js        |   2 +-
 .../TableDeliveryServiceRequestsController.js      |   4 +-
 .../TableDeliveryServicesController.js             |   2 +-
 .../FormEditDeliveryServiceRequestController.js    |   2 +-
 38 files changed, 2695 insertions(+), 829 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 625a52e..ce502e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Added ACME certificate renewals and ACME account registration using external account binding
 - Added functionality to automatically renew ACME certificates.
 - Added an endpoint for statuses on asynchronous jobs and applied it to the ACME renewal endpoint.
+- Traffic Ops API version 4.0
+- `GET` request method for `/deliveryservices/{{ID}}/assign`
+- `GET` request method for `/deliveryservices/{{ID}}/status`
 
 ### Fixed
 - [#5609](https://github.com/apache/trafficcontrol/issues/5609) - Fixed GET /servercheck filter for an extra query param.
diff --git a/docs/source/api/v2/deliveryservice_requests.rst b/docs/source/api/v2/deliveryservice_requests.rst
index 36ef8c1..e945229 100644
--- a/docs/source/api/v2/deliveryservice_requests.rst
+++ b/docs/source/api/v2/deliveryservice_requests.rst
@@ -31,37 +31,43 @@ Request Structure
 -----------------
 .. table:: Request Query Parameters
 
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| Name      | Required | Description                                                                              |
-	+===========+==========+==========================================================================================+
-	| assignee  | no       | Filter for :ref:`ds_requests` that are assigned to the user                              |
-	|           |          | identified by this username.                                                             |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| assigneeId| no       | Filter for :ref:`ds_requests` that are assigned to the user                              |
-	|           |          | identified by this integral, unique identifier                                           |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| author    | no       | Filter for :ref:`ds_requests` submitted by the user                                      |
-	|           |          | identified by this username                                                              |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| authorId  | no       | Filter for :ref:`ds_requests` submitted by the user                                      |
-	|           |          | identified by this integral, unique identifier                                           |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| changeType| no       | Filter for :ref:`ds_requests` of the change type specified.                              |
-	|           |          | Can be ``create``, ``update``, or ``delete``.                                            |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| createdAt | no       | Filter for :ref:`ds_requests` created on a certain date/time.                            |
-	|           |          | Value must be :rfc:`3339` compliant. Eg. 2019-09-19T19:35:38.828535Z                     |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| id        | no       | Filter for the :ref:`Delivery Service Request <ds_requests>` identified by this          |
-	|           |          | integral, unique identifier.                                                             |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| status    | no       | Filter for :ref:`ds_requests` whose status is the status                                 |
-	|           |          | specified. The status can be ``draft``, ``submitted``, ``pending``, ``rejected``, or     |
-	|           |          | ``complete``.                                                                            |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| xmlId     | no       | Filter for :ref:`ds_requests` that have the given                                        |
-	|           |          | :ref:`ds-xmlid`.                                                                         |
-	+-----------+----------+------------------------------------------------------------------------------------------+
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| Name      | Required | Description                                                                                                                             |
+	+===========+==========+=========================================================================================================================================+
+	| assignee  | no       | Filter for :ref:`ds_requests` that are assigned to the user identified by this username.                                                |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| assigneeId| no       | Filter for :ref:`ds_requests` that are assigned to the user identified by this integral, unique identifier                              |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| author    | no       | Filter for :ref:`ds_requests` submitted by the user identified by this username                                                         |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| authorId  | no       | Filter for :ref:`ds_requests` submitted by the user identified by this integral, unique identifier                                      |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| changeType| no       | Filter for :ref:`ds_requests` of the change type specified. Can be ``create``, ``update``, or ``delete``.                               |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| createdAt | no       | Filter for :ref:`ds_requests` created on a certain date/time. Value must be :rfc:`3339` compliant. Eg. ``2019-09-19T19:35:38.828535Z``  |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| id        | no       | Filter for the :ref:`Delivery Service Request <ds_requests>` identified by this integral, unique identifier.                            |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| status    | no       | Filter for :ref:`ds_requests` whose status is the status specified. The status can be ``draft``, ``submitted``, ``pending``,            |
+	|           |          | ``rejected``, or ``complete``.                                                                                                          |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| xmlId     | no       | Filter for :ref:`ds_requests` that have the given :ref:`ds-xmlid`.                                                                      |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| orderby   | no       | Choose the ordering of the results - must be the name of one of the fields of the objects in the ``response``                           |
+	|           |          | array                                                                                                                                   |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| sortOrder | no       | Changes the order of sorting. Either ascending (default or "asc") or descending ("desc")                                                |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| limit     | no       | Choose the maximum number of results to return                                                                                          |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| offset    | no       | The number of results to skip before beginning to return results. Must use in conjunction with limit                                    |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| page      | no       | Return the n\ :sup:`th` page of results, where "n" is the value of this parameter, pages are ``limit`` long and the first page is 1.    |
+	|           |          | If ``offset`` was defined, this query parameter has no effect. ``limit`` must be defined to make use of ``page``.                       |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+
+.. versionadded:: ATCv6
+	The ``createdAt`` query parameter was added to this in endpoint across all API versions in :abbr:`ATC (Apache Traffic Control)` version 6.0.0.
 
 .. code-block:: http
 	:caption: Request Example
diff --git a/docs/source/api/v3/deliveryservice_requests.rst b/docs/source/api/v3/deliveryservice_requests.rst
index c2fcb03..f2c2757 100644
--- a/docs/source/api/v3/deliveryservice_requests.rst
+++ b/docs/source/api/v3/deliveryservice_requests.rst
@@ -31,37 +31,43 @@ Request Structure
 -----------------
 .. table:: Request Query Parameters
 
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| Name      | Required | Description                                                                              |
-	+===========+==========+==========================================================================================+
-	| assignee  | no       | Filter for :ref:`ds_requests` that are assigned to the user                              |
-	|           |          | identified by this username.                                                             |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| assigneeId| no       | Filter for :ref:`ds_requests` that are assigned to the user                              |
-	|           |          | identified by this integral, unique identifier                                           |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| author    | no       | Filter for :ref:`ds_requests` submitted by the user                                      |
-	|           |          | identified by this username                                                              |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| authorId  | no       | Filter for :ref:`ds_requests` submitted by the user                                      |
-	|           |          | identified by this integral, unique identifier                                           |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| changeType| no       | Filter for :ref:`ds_requests` of the change type specified.                              |
-	|           |          | Can be ``create``, ``update``, or ``delete``.                                            |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| createdAt | no       | Filter for :ref:`ds_requests` created on a certain date/time.                            |
-	|           |          | Value must be :rfc:`3339` compliant. Eg. 2019-09-19T19:35:38.828535Z                     |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| id        | no       | Filter for the :ref:`Delivery Service Request <ds_requests>` identified by this          |
-	|           |          | integral, unique identifier.                                                             |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| status    | no       | Filter for :ref:`ds_requests` whose status is the status                                 |
-	|           |          | specified. The status can be ``draft``, ``submitted``, ``pending``, ``rejected``, or     |
-	|           |          | ``complete``.                                                                            |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| xmlId     | no       | Filter for :ref:`ds_requests` that have the given                                        |
-	|           |          | :ref:`ds-xmlid`.                                                                         |
-	+-----------+----------+------------------------------------------------------------------------------------------+
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| Name      | Required | Description                                                                                                                             |
+	+===========+==========+=========================================================================================================================================+
+	| assignee  | no       | Filter for :ref:`ds_requests` that are assigned to the user identified by this username.                                                |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| assigneeId| no       | Filter for :ref:`ds_requests` that are assigned to the user identified by this integral, unique identifier                              |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| author    | no       | Filter for :ref:`ds_requests` submitted by the user identified by this username                                                         |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| authorId  | no       | Filter for :ref:`ds_requests` submitted by the user identified by this integral, unique identifier                                      |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| changeType| no       | Filter for :ref:`ds_requests` of the change type specified. Can be ``create``, ``update``, or ``delete``.                               |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| createdAt | no       | Filter for :ref:`ds_requests` created on a certain date/time. Value must be :rfc:`3339` compliant. Eg. ``2019-09-19T19:35:38.828535Z``  |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| id        | no       | Filter for the :ref:`Delivery Service Request <ds_requests>` identified by this integral, unique identifier.                            |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| status    | no       | Filter for :ref:`ds_requests` whose status is the status specified. The status can be ``draft``, ``submitted``, ``pending``,            |
+	|           |          | ``rejected``, or ``complete``.                                                                                                          |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| xmlId     | no       | Filter for :ref:`ds_requests` that have the given :ref:`ds-xmlid`.                                                                      |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| orderby   | no       | Choose the ordering of the results - must be the name of one of the fields of the objects in the ``response``                           |
+	|           |          | array                                                                                                                                   |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| sortOrder | no       | Changes the order of sorting. Either ascending (default or "asc") or descending ("desc")                                                |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| limit     | no       | Choose the maximum number of results to return                                                                                          |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| offset    | no       | The number of results to skip before beginning to return results. Must use in conjunction with limit                                    |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| page      | no       | Return the n\ :sup:`th` page of results, where "n" is the value of this parameter, pages are ``limit`` long and the first page is 1.    |
+	|           |          | If ``offset`` was defined, this query parameter has no effect. ``limit`` must be defined to make use of ``page``.                       |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+
+.. versionadded:: ATCv6
+	The ``createdAt`` query parameter was added to this in endpoint across all API versions in :abbr:`ATC (Apache Traffic Control)` version 6.0.0.
 
 .. code-block:: http
 	:caption: Request Example
diff --git a/docs/source/api/v4/deliveryservice_requests.rst b/docs/source/api/v4/deliveryservice_requests.rst
index 91d5a21..f802ea4 100644
--- a/docs/source/api/v4/deliveryservice_requests.rst
+++ b/docs/source/api/v4/deliveryservice_requests.rst
@@ -31,37 +31,43 @@ Request Structure
 -----------------
 .. table:: Request Query Parameters
 
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| Name      | Required | Description                                                                              |
-	+===========+==========+==========================================================================================+
-	| assignee  | no       | Filter for :ref:`ds_requests` that are assigned to the user                              |
-	|           |          | identified by this username.                                                             |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| assigneeId| no       | Filter for :ref:`ds_requests` that are assigned to the user                              |
-	|           |          | identified by this integral, unique identifier                                           |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| author    | no       | Filter for :ref:`ds_requests` submitted by the user                                      |
-	|           |          | identified by this username                                                              |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| authorId  | no       | Filter for :ref:`ds_requests` submitted by the user                                      |
-	|           |          | identified by this integral, unique identifier                                           |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| changeType| no       | Filter for :ref:`ds_requests` of the change type specified.                              |
-	|           |          | Can be ``create``, ``update``, or ``delete``.                                            |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| createdAt | no       | Filter for :ref:`ds_requests` created on a certain date/time.                            |
-	|           |          | Value must be :rfc:`3339` compliant. Eg. 2019-09-19T19:35:38.828535Z                     |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| id        | no       | Filter for the :ref:`Delivery Service Request <ds_requests>` identified by this          |
-	|           |          | integral, unique identifier.                                                             |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| status    | no       | Filter for :ref:`ds_requests` whose status is the status                                 |
-	|           |          | specified. The status can be ``draft``, ``submitted``, ``pending``, ``rejected``, or     |
-	|           |          | ``complete``.                                                                            |
-	+-----------+----------+------------------------------------------------------------------------------------------+
-	| xmlId     | no       | Filter for :ref:`ds_requests` that have the given                                        |
-	|           |          | :ref:`ds-xmlid`.                                                                         |
-	+-----------+----------+------------------------------------------------------------------------------------------+
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| Name      | Required | Description                                                                                                                             |
+	+===========+==========+=========================================================================================================================================+
+	| assignee  | no       | Filter for :ref:`ds_requests` that are assigned to the user identified by this username.                                                |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| assigneeId| no       | Filter for :ref:`ds_requests` that are assigned to the user identified by this integral, unique identifier                              |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| author    | no       | Filter for :ref:`ds_requests` submitted by the user identified by this username                                                         |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| authorId  | no       | Filter for :ref:`ds_requests` submitted by the user identified by this integral, unique identifier                                      |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| changeType| no       | Filter for :ref:`ds_requests` of the change type specified. Can be ``create``, ``update``, or ``delete``.                               |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| createdAt | no       | Filter for :ref:`ds_requests` created on a certain date/time. Value must be :rfc:`3339` compliant. Eg. ``2019-09-19T19:35:38.828535Z``  |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| id        | no       | Filter for the :ref:`Delivery Service Request <ds_requests>` identified by this integral, unique identifier.                            |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| status    | no       | Filter for :ref:`ds_requests` whose status is the status specified. The status can be ``draft``, ``submitted``, ``pending``,            |
+	|           |          | ``rejected``, or ``complete``.                                                                                                          |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| xmlId     | no       | Filter for :ref:`ds_requests` that have the given :ref:`ds-xmlid`.                                                                      |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| orderby   | no       | Choose the ordering of the results - must be the name of one of the fields of the objects in the ``response``                           |
+	|           |          | array                                                                                                                                   |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| sortOrder | no       | Changes the order of sorting. Either ascending (default or "asc") or descending ("desc")                                                |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| limit     | no       | Choose the maximum number of results to return                                                                                          |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| offset    | no       | The number of results to skip before beginning to return results. Must use in conjunction with limit                                    |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+	| page      | no       | Return the n\ :sup:`th` page of results, where "n" is the value of this parameter, pages are ``limit`` long and the first page is 1.    |
+	|           |          | If ``offset`` was defined, this query parameter has no effect. ``limit`` must be defined to make use of ``page``.                       |
+	+-----------+----------+-----------------------------------------------------------------------------------------------------------------------------------------+
+
+.. versionadded:: ATCv6
+	The ``createdAt`` query parameter was added to this in endpoint across all API versions in :abbr:`ATC (Apache Traffic Control)` version 6.0.0.
 
 .. code-block:: http
 	:caption: Request Example
diff --git a/docs/source/api/v4/deliveryservice_requests_id_assign.rst b/docs/source/api/v4/deliveryservice_requests_id_assign.rst
index 14bcf41..91946a1 100644
--- a/docs/source/api/v4/deliveryservice_requests_id_assign.rst
+++ b/docs/source/api/v4/deliveryservice_requests_id_assign.rst
@@ -20,6 +20,57 @@
 ******************************************
 Assign a :term:`Delivery Service Request` to a user.
 
+``GET``
+=======
+.. versionadded:: 4.0
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Response Type:  Object (string)
+
+Request Structure
+-----------------
+.. table:: Request Path Parameters
+
+	+------+-----------------------------------------------------------------------------------------------------------------+
+	| Name | Description                                                                                                     |
+	+======+=================================================================================================================+
+	|  ID  | The integral, unique identifier of the :term:`Delivery Service Request` for which assignment is being retrieved |
+	+------+-----------------------------------------------------------------------------------------------------------------+
+
+.. code-block:: http
+	:caption: Request Example
+
+	GET /api/4.0/deliveryservice_requests/1/assign HTTP/1.1
+	User-Agent: python-requests/2.24.0
+	Accept-Encoding: gzip, deflate
+	Accept: */*
+	Connection: keep-alive
+	Cookie: mojolicious=...
+
+Response Structure
+------------------
+The response is the username of the user to whom the :term:`Delivery Service Request` is assigned - or ``null`` if it is unassigned.
+
+.. code-block:: http
+	:caption: Response Example
+
+	HTTP/1.1 200 OK
+	Access-Control-Allow-Credentials: true
+	Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
+	Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
+	Access-Control-Allow-Origin: *
+	Content-Encoding: gzip
+	Content-Type: application/json
+	Set-Cookie: mojolicious=...; Path=/; Expires=Tue, 02 Feb 2021 22:48:48 GMT; Max-Age=3600; HttpOnly
+	Vary: Accept-Encoding
+	X-Server-Name: traffic_ops_golang/
+	Date: Tue, 02 Feb 2021 21:48:48 GMT
+	Content-Length: 45
+
+	{ "response": "admin" }
+
+
 ``PUT``
 =======
 :Auth. Required: Yes
@@ -28,24 +79,37 @@ Assign a :term:`Delivery Service Request` to a user.
 
 Request Structure
 -----------------
-:id:            The integral, unique identifier assigned to the :term:`DSR <Delivery Service Request>`
-:assignee:      The username of the user to whom the :term:`Delivery Service Request` is assigned.
+.. table:: Request Path Parameters
+
+	+------+----------------------------------------------------------------------------------------+
+	| Name | Description                                                                            |
+	+======+========================================================================================+
+	|  ID  | The integral, unique identifier of the :term:`Delivery Service Request` being assigned |
+	+------+----------------------------------------------------------------------------------------+
+
+:assignee: The username of the user to whom the :term:`Delivery Service Request` is assigned
+
+	.. versionadded:: 4.0
+
+:assigneeId: The integral, unique identifier of the user to whom the :term:`Delivery Service Request` is assigned
+
+	.. versionchanged:: 4.0
+		Prior to APIv4.0, this was the only property that could be used to change a :term:`Delivery Service Request`'s Assignee - and thus was a required field.
+
+Only one of ``assignee`` or ``assigneeId`` must be given. If both are present in a request, ``assigneeId`` takes precedence.
 
 .. code-block:: http
 	:caption: Request Example
 
 	PUT /api/4.0/deliveryservice_requests/1/assign HTTP/1.1
-	User-Agent: python-requests/2.22.0
+	User-Agent: python-requests/2.24.0
 	Accept-Encoding: gzip, deflate
 	Accept: */*
 	Connection: keep-alive
 	Cookie: mojolicious=...
-	Content-Length: 28
+	Content-Length: 20
 
-	{
-		"id": 1,
-		"assigneeId": 2
-	}
+	{"assignee": "admin"}
 
 Response Structure
 ------------------
@@ -153,12 +217,10 @@ Response Structure
 	Content-Length: 931
 
 	{
-		"alerts": [
-			{
-				"text": "deliveryservice_request was updated.",
-				"level": "success"
-			}
-		],
+		"alerts": [{
+			"text": "Changed assignee of 'demo1' Delivery Service Request to 'admin'",
+			"level": "success"
+		}],
 		"response": {
 			"assigneeId": 2,
 			"assignee": "admin",
diff --git a/docs/source/api/v4/deliveryservice_requests_id_status.rst b/docs/source/api/v4/deliveryservice_requests_id_status.rst
index 3fe18b2..2c2cc5d 100644
--- a/docs/source/api/v4/deliveryservice_requests_id_status.rst
+++ b/docs/source/api/v4/deliveryservice_requests_id_status.rst
@@ -18,7 +18,61 @@
 ******************************************
 ``deliveryservice_requests/{{ID}}/status``
 ******************************************
-Sets the status of a :term:`Delivery Service Request`.
+Get or set the status of a :term:`Delivery Service Request`.
+
+``GET``
+=======
+Gets the status of a :term:`DSR`.
+
+.. versionadded:: 4.0
+
+:Auth. Required: Yes
+:Roles Required: "admin", "Federation", "operations", "Portal", or "Steering"
+:Response Type:  Object (string)
+
+Request Structure
+-----------------
+.. table:: Request Path Parameters
+
+	+------+-----------------------------------------------------------------------------------------+
+	| Name | Description                                                                             |
+	+======+=========================================================================================+
+	|  ID  | The integral, unique identifier of the :term:`Delivery Service Request` being inspected |
+	+------+-----------------------------------------------------------------------------------------+
+
+
+.. code-block:: http
+	:caption: Request Example
+
+	GET /api/4.0/deliveryservice_requests/1/status HTTP/1.1
+	User-Agent: python-requests/2.24.0
+	Accept-Encoding: gzip, deflate
+	Accept: */*
+	Connection: keep-alive
+	Cookie: mojolicious=...
+
+Response Structure
+------------------
+The response is the status of the requested :term:`DSR`.
+
+.. code-block:: http
+	:caption: Response Example
+
+	HTTP/1.1 200 OK
+	Access-Control-Allow-Credentials: true
+	Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
+	Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
+	Access-Control-Allow-Origin: *
+	Content-Encoding: gzip
+	Content-Type: application/json
+	Set-Cookie: mojolicious=...; Path=/; Expires=Tue, 02 Feb 2021 22:56:47 GMT; Max-Age=3600; HttpOnly
+	Vary: Accept-Encoding
+	X-Server-Name: traffic_ops_golang/
+	Date: Tue, 02 Feb 2021 21:56:47 GMT
+	Content-Length: 45
+
+	{ "response": "draft" }
+
 
 ``PUT``
 =======
@@ -28,8 +82,16 @@ Sets the status of a :term:`Delivery Service Request`.
 
 Request Structure
 -----------------
-:id:            The integral, unique identifier assigned to the :term:`DSR <Delivery Service Request>`
-:status:        The status of the `DSR <Delivery Service Request>`. Can be "draft", "submitted", "rejected", "pending", or "complete".
+.. table:: Request Path Parameters
+
+	+------+-----------------------------------------------------------------------------------------+
+	| Name | Description                                                                             |
+	+======+=========================================================================================+
+	|  ID  | The integral, unique identifier of the :term:`Delivery Service Request` being modified  |
+	+======+=========================================================================================+
+
+
+:status: The status of the :term:`DSR`. Can be "draft", "submitted", "rejected", "pending", or "complete".
 
 .. code-block:: http
 	:caption: Request Example
@@ -43,7 +105,6 @@ Request Structure
 	Content-Length: 28
 
 	{
-		"id": 1,
 		"status": "rejected"
 	}
 
@@ -154,12 +215,10 @@ Response Structure
 	Content-Length: 930
 
 	{
-		"alerts": [
-			{
-				"text": "deliveryservice_request was updated.",
-				"level": "success"
-			}
-		],
+		"alerts": [{
+			"text": "Changed status of 'demo1' Delivery Service Request from 'draft' to 'submitted'",
+			"level": "success"
+		}],
 		"response": {
 			"assigneeId": 2,
 			"assignee": "admin",
@@ -256,7 +315,7 @@ Response Structure
 				"maxOriginConnections": 0,
 				"ecsEnabled": false
 			},
-			"status": "rejected"
+			"status": "submitted"
 		}
 	}
 
diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst
index aea7f42..7714743 100644
--- a/docs/source/glossary.rst
+++ b/docs/source/glossary.rst
@@ -115,7 +115,9 @@ Glossary
 		.. seealso:: See :ref:`delivery-services` for a more in-depth explanation of :dfn:`Delivery Services`.
 
 	Delivery Service Request
+	Delivery Service Requests
 	DSR
+	DSRs
 		A :dfn:`Delivery Service Request` is the result of attempting to modify a :term:`Delivery Service` when ``dsRequests.enabled`` is set to ``true`` in ``traffic_portal_properties.json``. See :ref:`ds_requests` for more information.
 
 	Delivery Service required capabilities
diff --git a/lib/go-rfc/cachecontrol.go b/lib/go-rfc/cachecontrol.go
index f2ddc58..57151c4 100644
--- a/lib/go-rfc/cachecontrol.go
+++ b/lib/go-rfc/cachecontrol.go
@@ -107,8 +107,5 @@ func ParseETags(eTags []string) (time.Time, bool) {
 			latestTime = et
 		}
 	}
-	if latestTime == (time.Time{}) {
-		return time.Time{}, false
-	}
-	return latestTime, true
+	return latestTime, latestTime != time.Time{}
 }
diff --git a/lib/go-rfc/http.go b/lib/go-rfc/http.go
index 586328a..4baeead 100644
--- a/lib/go-rfc/http.go
+++ b/lib/go-rfc/http.go
@@ -50,6 +50,10 @@ const (
 	Gzip                      = "gzip"                     // RFC7230§4.2.3
 )
 
+// LastModifiedFormat is the format used by dates in the HTTP Last-Modified
+// header.
+const LastModifiedFormat = "Mon, 02 Jan 2006 15:04:05 MST" // RFC1123
+
 // ValidHTTPCodes provides fast lookup of whether a HTTP response code is valid.
 var ValidHTTPCodes = map[int]struct{}{
 	http.StatusContinue:           {}, // RFC 7231, 6.2.1
diff --git a/lib/go-tc/deliveryservice_requests.go b/lib/go-tc/deliveryservice_requests.go
index 75a480a..6830361 100644
--- a/lib/go-tc/deliveryservice_requests.go
+++ b/lib/go-tc/deliveryservice_requests.go
@@ -16,6 +16,7 @@ package tc
 */
 
 import (
+	"database/sql"
 	"database/sql/driver"
 	"encoding/json"
 	"errors"
@@ -23,11 +24,12 @@ import (
 	"html/template"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-util"
 
-	"github.com/go-ozzo/ozzo-validation"
+	validation "github.com/go-ozzo/ozzo-validation"
 	"github.com/go-ozzo/ozzo-validation/is"
 )
 
@@ -406,22 +408,80 @@ type DeliveryServiceRequest struct {
 // DeliveryServiceRequestNullable is used as part of the workflow to create,
 // modify, or delete a delivery service.
 type DeliveryServiceRequestNullable struct {
-	AssigneeID      *int               `json:"assigneeId,omitempty" db:"assignee_id"`
-	Assignee        *string            `json:"assignee,omitempty"`
-	AuthorID        *IDNoMod           `json:"authorId" db:"author_id"`
-	Author          *string            `json:"author"`
-	ChangeType      *string            `json:"changeType" db:"change_type"`
-	CreatedAt       *TimeNoMod         `json:"createdAt" db:"created_at"`
-	ID              *int               `json:"id" db:"id"`
-	LastEditedBy    *string            `json:"lastEditedBy"`
-	LastEditedByID  *IDNoMod           `json:"lastEditedById" db:"last_edited_by_id"`
-	LastUpdated     *TimeNoMod         `json:"lastUpdated" db:"last_updated"`
-	DeliveryService *DeliveryServiceV4 `json:"deliveryService" db:"deliveryservice"`
-	Status          *RequestStatus     `json:"status" db:"status"`
-	XMLID           *string            `json:"-" db:"xml_id"`
+	AssigneeID      *int                        `json:"assigneeId,omitempty" db:"assignee_id"`
+	Assignee        *string                     `json:"assignee,omitempty"`
+	AuthorID        *IDNoMod                    `json:"authorId" db:"author_id"`
+	Author          *string                     `json:"author"`
+	ChangeType      *string                     `json:"changeType" db:"change_type"`
+	CreatedAt       *TimeNoMod                  `json:"createdAt" db:"created_at"`
+	ID              *int                        `json:"id" db:"id"`
+	LastEditedBy    *string                     `json:"lastEditedBy"`
+	LastEditedByID  *IDNoMod                    `json:"lastEditedById" db:"last_edited_by_id"`
+	LastUpdated     *TimeNoMod                  `json:"lastUpdated" db:"last_updated"`
+	DeliveryService *DeliveryServiceNullableV30 `json:"deliveryService" db:"deliveryservice"`
+	Status          *RequestStatus              `json:"status" db:"status"`
+	XMLID           *string                     `json:"-" db:"xml_id"`
 }
 
-// UnmarshalJSON implements the json.Unmarshaller interface to suppress unmarshalling for IDNoMod
+// Upgrade coerces the DeliveryServiceRequestNullable to the newer
+// DeliveryServiceRequestV40 structure.
+//
+// All reference properties are "deep"-copied so they may be modified without
+// affecting the original. However, DeliveryService is constructed as a "deep"
+// copy, but the properties of the underlying DeliveryServiceNullableV30 are
+// "shallow" copied, and so modifying them *can* affect the original and
+// vice-versa.
+func (dsr DeliveryServiceRequestNullable) Upgrade() DeliveryServiceRequestV40 {
+	var upgraded DeliveryServiceRequestV40
+	if dsr.Assignee != nil {
+		upgraded.Assignee = new(string)
+		*upgraded.Assignee = *dsr.Assignee
+	}
+	if dsr.AssigneeID != nil {
+		upgraded.AssigneeID = new(int)
+		*upgraded.AssigneeID = *dsr.AssigneeID
+	}
+	if dsr.Author != nil {
+		upgraded.Author = *dsr.Author
+	}
+	if dsr.AuthorID != nil {
+		upgraded.AuthorID = new(int)
+		*upgraded.AuthorID = int(*dsr.AuthorID)
+	}
+	if dsr.ChangeType != nil {
+		upgraded.ChangeType = DSRChangeType(*dsr.ChangeType)
+	}
+	if dsr.CreatedAt != nil {
+		upgraded.CreatedAt = dsr.CreatedAt.Time
+	}
+	if dsr.DeliveryService != nil {
+		upgraded.DeliveryService = new(DeliveryServiceV4)
+		*upgraded.DeliveryService = dsr.DeliveryService.UpgradeToV4()
+	}
+	if dsr.ID != nil {
+		upgraded.ID = new(int)
+		*upgraded.ID = *dsr.ID
+	}
+	if dsr.LastEditedBy != nil {
+		upgraded.LastEditedBy = *dsr.LastEditedBy
+	}
+	if dsr.LastEditedByID != nil {
+		upgraded.LastEditedByID = new(int)
+		*upgraded.LastEditedByID = int(*dsr.LastEditedByID)
+	}
+	if dsr.Status != nil {
+		upgraded.Status = *dsr.Status
+	}
+	if dsr.XMLID != nil {
+		upgraded.XMLID = *dsr.XMLID
+	} else if dsr.DeliveryService != nil && dsr.DeliveryService.XMLID != nil {
+		upgraded.XMLID = *dsr.DeliveryService.XMLID
+	}
+	return upgraded
+}
+
+// UnmarshalJSON implements the json.Unmarshaller interface to suppress
+// unmarshalling for IDNoMod.
 func (a *IDNoMod) UnmarshalJSON([]byte) error {
 	return nil
 }
@@ -444,7 +504,12 @@ const (
 	RequestStatusComplete = RequestStatus("complete")
 )
 
-// RequestStatuses -- user-visible string associated with each of the above
+// String returns the string value of the Request Status.
+func (r RequestStatus) String() string {
+	return string(r)
+}
+
+// RequestStatuses -- user-visible string associated with each of the above.
 var RequestStatuses = []RequestStatus{
 	// "invalid" -- don't list here..
 	"draft",
@@ -454,7 +519,7 @@ var RequestStatuses = []RequestStatus{
 	"complete",
 }
 
-// UnmarshalJSON implements json.Unmarshaller
+// UnmarshalJSON implements json.Unmarshaller.
 func (r *RequestStatus) UnmarshalJSON(b []byte) error {
 	u, err := strconv.Unquote(string(b))
 	if err != nil {
@@ -469,12 +534,12 @@ func (r *RequestStatus) UnmarshalJSON(b []byte) error {
 	return json.Unmarshal(b, (*string)(r))
 }
 
-// MarshalJSON implements json.Marshaller
+// MarshalJSON implements json.Marshaller.
 func (r RequestStatus) MarshalJSON() ([]byte, error) {
 	return json.Marshal(string(r))
 }
 
-// Value implements driver.Valuer
+// Value implements driver.Valuer.
 func (r *RequestStatus) Value() (driver.Value, error) {
 	v, err := json.Marshal(r)
 	log.Debugf("value is %v; err is %v", v, err)
@@ -482,7 +547,7 @@ func (r *RequestStatus) Value() (driver.Value, error) {
 	return v, err
 }
 
-// Scan implements sql.Scanner
+// Scan implements sql.Scanner.
 func (r *RequestStatus) Scan(src interface{}) error {
 	b, ok := src.([]byte)
 	if !ok {
@@ -492,7 +557,7 @@ func (r *RequestStatus) Scan(src interface{}) error {
 	return json.Unmarshal(b, r)
 }
 
-// RequestStatusFromString gets the status enumeration from a string
+// RequestStatusFromString gets the status enumeration from a string.
 func RequestStatusFromString(rs string) (RequestStatus, error) {
 	if rs == "" {
 		return RequestStatusDraft, nil
@@ -505,7 +570,8 @@ func RequestStatusFromString(rs string) (RequestStatus, error) {
 	return RequestStatusInvalid, errors.New(rs + " is not a valid RequestStatus name")
 }
 
-// ValidTransition returns nil if the transition is allowed for the workflow, an error if not
+// ValidTransition returns nil if the transition is allowed for the workflow,
+// an error if not.
 func (r RequestStatus) ValidTransition(to RequestStatus) error {
 	if r == RequestStatusRejected || r == RequestStatusComplete {
 		// once rejected or completed,  no changes allowed
@@ -547,3 +613,259 @@ func (r RequestStatus) ValidTransition(to RequestStatus) error {
 	}
 	return errors.New("invalid transition from " + string(r) + " to " + string(to))
 }
+
+// DSRChangeType is an "enumerated" string type that encodes the legal values of
+// a Delivery Service Request's Change Type.
+type DSRChangeType string
+
+// These are the valid values for Delivery Service Request Change Types.
+const (
+	// The original Delivery Service is being modified to match the requested
+	// one.
+	DSRChangeTypeUpdate = DSRChangeType("update")
+	// The requested Delivery Service is being created.
+	DSRChangeTypeCreate = DSRChangeType("create")
+	// The requested Delivery Service is being deleted.
+	DSRChangeTypeDelete = DSRChangeType("delete")
+)
+
+// DSRChangeTypeFromString converts the passed string to a DSRChangeType
+// (case-insensitive), returning an error if the string is not a valid
+// Delivery Service Request Change Type.
+func DSRChangeTypeFromString(s string) (DSRChangeType, error) {
+	switch strings.ToLower(s) {
+	case "update":
+		return DSRChangeTypeUpdate, nil
+	case "create":
+		return DSRChangeTypeCreate, nil
+	case "delete":
+		return DSRChangeTypeDelete, nil
+	}
+	return "INVALID", fmt.Errorf("invalid Delivery Service Request changeType: '%s'", s)
+}
+
+// String implements the fmt.Stringer interface, returning a textual
+// representation of the DSRChangeType.
+func (dsrct DSRChangeType) String() string {
+	return string(dsrct)
+}
+
+// MarshalJSON implements the encoding/json.Marshaller interface.
+func (dsrct DSRChangeType) MarshalJSON() ([]byte, error) {
+	return json.Marshal(string(dsrct))
+}
+
+// UnmarshalJSON implements the encoding/json.Unmarshaller interface.
+func (dsrct *DSRChangeType) UnmarshalJSON(b []byte) error {
+	// This should only happen if this method is called directly; encoding/json
+	// itself guards against this.
+	if dsrct == nil {
+		return errors.New("UnmarshalJSON(nil *tc.DSRChangeType)")
+	}
+
+	ctStr, err := strconv.Unquote(string(b))
+	if err != nil {
+		return err
+	}
+
+	ct, err := DSRChangeTypeFromString(ctStr)
+	if err != nil {
+		return err
+	}
+	*dsrct = ct
+	return nil
+}
+
+// DeliveryServiceRequestV40 is the type of a Delivery Service Request in
+// Traffic Ops API version 4.0.
+type DeliveryServiceRequestV40 struct {
+	// Assignee is the username of the user assigned to the Delivery Service
+	// Request, if any.
+	Assignee *string `json:"assignee"`
+	// AssigneeID is the integral, unique identifier of the user assigned to the
+	// Delivery Service Request, if any.
+	AssigneeID *int `json:"assigneeId" db:"assignee_id"`
+	// Author is the username of the user who created the Delivery Service
+	// Request.
+	Author string `json:"author"`
+	// AuthorID is the integral, unique identifier of the user who created the
+	// Delivery Service Request, if/when it is known.
+	AuthorID *int `json:"authorId" db:"author_id"`
+	// ChangeType represents the type of change being made, must be one of
+	// "create", "change" or "delete".
+	ChangeType DSRChangeType `json:"changeType" db:"change_type"`
+	// CreatedAt is the date/time at which the Delivery Service Request was
+	// created.
+	CreatedAt time.Time `json:"createdAt" db:"created_at"`
+	// ID is the integral, unique identifier for the Delivery Service Request
+	// if/when it is known.
+	ID *int `json:"id" db:"id"`
+	// LastEditedBy is the username of the user by whom the Delivery Service
+	// Request was last edited.
+	LastEditedBy string `json:"lastEditedBy"`
+	// LastEditedByID is the integral, unique identifier of the user by whom the
+	// Delivery Service Request was last edited, if/when it is known.
+	LastEditedByID *int `json:"lastEditedById" db:"last_edited_by_id"`
+	// LastUpdated is the date/time at which the Delivery Service was last
+	// modified.
+	LastUpdated time.Time `json:"lastUpdated" db:"last_updated"`
+	// DeliveryService is the requested Delivery Service; its exact meaning is
+	// dependent on 'ChangeType'.
+	DeliveryService *DeliveryServiceV4 `json:"deliveryService" db:"deliveryservice"`
+	// Status is the status of the Delivery Service Request.
+	Status RequestStatus `json:"status" db:"status"`
+	// Used internally to define the affected Delivery Service.
+	XMLID string `json:"-"`
+}
+
+type DeliveryServiceRequestV4 = DeliveryServiceRequestV40
+
+// IsOpen returns whether or not the Delivery Service Request is still "open" -
+// i.e. has not been rejected or completed.
+func (dsr DeliveryServiceRequestV40) IsOpen() bool {
+	return !dsr.IsClosed()
+}
+
+// IsClosed returns whether or not the Delivery Service Request has been
+// "closed", by being either rejected or completed.
+func (dsr DeliveryServiceRequestV40) IsClosed() bool {
+	return dsr.Status == RequestStatusComplete || dsr.Status == RequestStatusRejected || dsr.Status == RequestStatusPending
+}
+
+// Downgrade coerces the DeliveryServiceRequestV40 to the older
+// DeliveryServiceRequestNullable structure.
+//
+// "XMLID" will be copied directly if it is non-empty, otherwise determined
+// from the DeliveryService (if it's not 'nil').
+//
+// All reference properties are "deep"-copied so they may be modified without
+// affecting the original. However, DeliveryService is constructed as a "deep"
+// copy of "Requested", but the properties of the underlying
+// DeliveryServiceNullableV30 are "shallow" copied, and so modifying them *can*
+// affect the original and vice-versa.
+func (dsr DeliveryServiceRequestV40) Downgrade() DeliveryServiceRequestNullable {
+	downgraded := DeliveryServiceRequestNullable{
+		Author:       new(string),
+		ChangeType:   new(string),
+		LastEditedBy: new(string),
+		Status:       new(RequestStatus),
+	}
+	if dsr.Assignee != nil {
+		downgraded.Assignee = new(string)
+		*downgraded.Assignee = *dsr.Assignee
+	}
+	if dsr.AssigneeID != nil {
+		downgraded.AssigneeID = new(int)
+		*downgraded.AssigneeID = *dsr.AssigneeID
+	}
+	*downgraded.Author = dsr.Author
+	if dsr.AuthorID != nil {
+		downgraded.AuthorID = new(IDNoMod)
+		*downgraded.AuthorID = IDNoMod(*dsr.AuthorID)
+	}
+	*downgraded.ChangeType = dsr.ChangeType.String()
+	downgraded.CreatedAt = TimeNoModFromTime(dsr.CreatedAt)
+	if dsr.DeliveryService != nil {
+		downgraded.DeliveryService = new(DeliveryServiceNullableV30)
+		*downgraded.DeliveryService = dsr.DeliveryService.DowngradeToV3()
+	}
+	if dsr.ID != nil {
+		downgraded.ID = new(int)
+		*downgraded.ID = *dsr.ID
+	}
+	*downgraded.LastEditedBy = dsr.LastEditedBy
+	if dsr.LastEditedByID != nil {
+		downgraded.LastEditedByID = new(IDNoMod)
+		*downgraded.LastEditedByID = IDNoMod(*dsr.LastEditedByID)
+	}
+	downgraded.LastUpdated = TimeNoModFromTime(dsr.LastUpdated)
+	*downgraded.Status = dsr.Status
+	if dsr.XMLID != "" {
+		downgraded.XMLID = new(string)
+		*downgraded.XMLID = dsr.XMLID
+	} else if dsr.DeliveryService != nil && dsr.DeliveryService.XMLID != nil {
+		downgraded.XMLID = new(string)
+		*downgraded.XMLID = *dsr.DeliveryService.XMLID
+	}
+	return downgraded
+}
+
+// String encodes the DeliveryServiceRequestV40 as a string, in the format
+// "DeliveryServiceRequestV40({{Property}}={{Value}}[, {{Property}}={{Value}}]+)".
+//
+// If a property is a pointer value, then its dereferenced value is used -
+// unless it's nil, in which case "<nil>" is used as the value. DeliveryService
+// is omitted, because of how large it is. Times are formatted in RFC3339 format.
+func (dsr DeliveryServiceRequestV40) String() string {
+	var builder strings.Builder
+	builder.Write([]byte("DeliveryServiceRequestV40(Assignee="))
+	if dsr.Assignee != nil {
+		builder.WriteRune('"')
+		builder.WriteString(*dsr.Assignee)
+		builder.WriteRune('"')
+	} else {
+		builder.Write([]byte("<nil>"))
+	}
+	builder.Write([]byte(", AssigneeID="))
+	if dsr.AssigneeID != nil {
+		builder.WriteString(strconv.Itoa(*dsr.AssigneeID))
+	} else {
+		builder.Write([]byte("<nil>"))
+	}
+	builder.Write([]byte(`, Author="`))
+	builder.WriteString(dsr.Author)
+	builder.Write([]byte(`", AuthorID=`))
+	if dsr.AuthorID != nil {
+		builder.WriteString(strconv.Itoa(*dsr.AuthorID))
+	} else {
+		builder.Write([]byte("<nil>"))
+	}
+	builder.Write([]byte(`, ChangeType="`))
+	builder.WriteString(dsr.ChangeType.String())
+	builder.Write([]byte(`", CreatedAt=`))
+	builder.WriteString(dsr.CreatedAt.Format(time.RFC3339))
+	builder.Write([]byte(", ID="))
+	if dsr.ID != nil {
+		builder.WriteString(strconv.Itoa(*dsr.ID))
+	} else {
+		builder.Write([]byte("<nil>"))
+	}
+	builder.Write([]byte(`, LastEditedBy="`))
+	builder.WriteString(dsr.LastEditedBy)
+	builder.Write([]byte(`", LastEditedByID=`))
+	if dsr.LastEditedByID != nil {
+		builder.WriteString(strconv.Itoa(*dsr.LastEditedByID))
+	} else {
+		builder.Write([]byte("<nil>"))
+	}
+	builder.Write([]byte(`, LastUpdated=`))
+	builder.WriteString(dsr.LastUpdated.Format(time.RFC3339))
+	builder.Write([]byte(`, Status="`))
+	builder.WriteString(dsr.Status.String())
+	builder.Write([]byte(`")`))
+	return builder.String()
+}
+
+// SetXMLID sets the DeliveryServiceRequestV40's XMLID based on its DeliveryService.
+func (dsr *DeliveryServiceRequestV40) SetXMLID() {
+	if dsr == nil {
+		return
+	}
+	if dsr.DeliveryService != nil && dsr.DeliveryService.XMLID != nil {
+		dsr.XMLID = *dsr.DeliveryService.XMLID
+	}
+}
+
+// StatusChangeRequest is the form of a PUT request body to
+// /deliveryservice_requests/{{ID}}/status.
+type StatusChangeRequest struct {
+	// Status is the desired new status of the DSR.
+	Status RequestStatus `json:"status"`
+}
+
+// Validate satisfies the
+// github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api.ParseValidator
+// interface.
+func (*StatusChangeRequest) Validate(*sql.Tx) error {
+	return nil
+}
diff --git a/lib/go-tc/deliveryservice_requests_test.go b/lib/go-tc/deliveryservice_requests_test.go
index ab9e006..4d45f89 100644
--- a/lib/go-tc/deliveryservice_requests_test.go
+++ b/lib/go-tc/deliveryservice_requests_test.go
@@ -23,7 +23,9 @@ import (
 	"bytes"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"testing"
+	"time"
 )
 
 func TestStatus(t *testing.T) {
@@ -105,3 +107,119 @@ func TestRequestStatusJSON(t *testing.T) {
 		t.Errorf("expected %v, got %v", RequestStatusDraft, r)
 	}
 }
+
+func ExampleDSRChangeType_UnmarshalJSON() {
+	var dsrct DSRChangeType
+	raw := `"CREATE"`
+	if err := json.Unmarshal([]byte(raw), &dsrct); err != nil {
+		fmt.Printf("Error: %v\n", err)
+		return
+	}
+	fmt.Printf("Parsed DSRCT: '%s'\n", dsrct.String())
+
+	raw = `"something invalid"`
+	if err := json.Unmarshal([]byte(raw), &dsrct); err != nil {
+		fmt.Printf("Error: %v\n", err)
+		return
+	}
+	fmt.Printf("Parsed DSRCT: '%s'\n", dsrct.String())
+
+	// Output: Parsed DSRCT: 'create'
+	// Error: invalid Delivery Service Request changeType: 'something invalid'
+}
+
+func ExampleDeliveryServiceRequestV40_String() {
+	var dsr DeliveryServiceRequestV40
+	fmt.Println(dsr.String())
+
+	// Output: DeliveryServiceRequestV40(Assignee=<nil>, AssigneeID=<nil>, Author="", AuthorID=<nil>, ChangeType="", CreatedAt=0001-01-01T00:00:00Z, ID=<nil>, LastEditedBy="", LastEditedByID=<nil>, LastUpdated=0001-01-01T00:00:00Z, Status="")
+}
+
+func TestDeliveryServiceRequestV40_Downgrade(t *testing.T) {
+	xmlid := "xmlid"
+	dsr := DeliveryServiceRequestV40{
+		Assignee:        nil,
+		AssigneeID:      nil,
+		Author:          "author",
+		AuthorID:        nil,
+		ChangeType:      DSRChangeTypeCreate,
+		CreatedAt:       time.Time{},
+		ID:              nil,
+		LastEditedBy:    "last edited by",
+		LastEditedByID:  nil,
+		LastUpdated:     time.Now(),
+		DeliveryService: &DeliveryServiceV4{},
+		Status:          RequestStatusComplete,
+	}
+	dsr.DeliveryService.XMLID = &xmlid
+
+	downgraded := dsr.Downgrade()
+	if downgraded.Assignee != nil {
+		t.Errorf("Incorrect Assignee; want: <nil>, got: %s", *downgraded.Assignee)
+	}
+	if downgraded.AssigneeID != nil {
+		t.Errorf("Incorrect Assignee ID; want: <nil>, got: %d", *downgraded.AssigneeID)
+	}
+	if downgraded.Author == nil {
+		t.Errorf("Incorrect Author; want: '%s', got: <nil>", dsr.Author)
+	} else if *downgraded.Author != dsr.Author {
+		t.Errorf("Incorrect Author; want: '%s', got: '%s'", dsr.Author, *downgraded.Author)
+	}
+	if downgraded.AuthorID != nil {
+		t.Errorf("Incorrect AuthorID; want: <nil>, got: %v", *downgraded.AuthorID)
+	}
+	if downgraded.ChangeType == nil {
+		t.Errorf("Incorrect ChangeType; want: '%s', got: <nil>", dsr.ChangeType)
+	} else if *downgraded.ChangeType != dsr.ChangeType.String() {
+		t.Errorf("Incorrect ChangeType; want: '%s', got: '%s'", dsr.ChangeType, *downgraded.ChangeType)
+	}
+	if downgraded.CreatedAt == nil {
+		t.Errorf("Incorrect CreatedAt; want: %v, got: <nil>", dsr.CreatedAt)
+	} else if !dsr.CreatedAt.Equal(downgraded.CreatedAt.Time) {
+		t.Errorf("Incorrect CreatedAt; want: %v, got: %v", dsr.CreatedAt, *downgraded.CreatedAt)
+	}
+	if downgraded.DeliveryService == nil {
+		t.Errorf("DeliveryService was unexpectedly nil")
+	}
+	if downgraded.ID != nil {
+		t.Errorf("Incorrect ID; want: <nil>, got: %d", *downgraded.ID)
+	}
+	if downgraded.LastEditedBy == nil {
+		t.Errorf("Incorrect LastEditedBy; want: '%s', got: <nil>", dsr.LastEditedBy)
+	} else if *downgraded.LastEditedBy != dsr.LastEditedBy {
+		t.Errorf("Incorrect LastEditedBy; want: '%s', got: '%s'", dsr.LastEditedBy, *downgraded.LastEditedBy)
+	}
+	if downgraded.LastEditedByID != nil {
+		t.Errorf("Incorrect LastEditedByID; want: <nil>, got: %d", *downgraded.LastEditedByID)
+	}
+	if downgraded.LastUpdated == nil {
+		t.Errorf("Incorrect LastUpdated; want: %v, got: <nil>", dsr.LastUpdated)
+	} else if !dsr.LastUpdated.Equal(downgraded.LastUpdated.Time) {
+		t.Errorf("Incorrect LastUpdated; want: %v, got: %v", dsr.LastUpdated, *downgraded.LastUpdated)
+	}
+	if downgraded.Status == nil {
+		t.Errorf("Incorrect Status; want: '%s', got: <nil>", dsr.Status)
+	} else if *downgraded.Status != dsr.Status {
+		t.Errorf("Incorrect Status; want: '%s', got: '%s'", dsr.Status, *downgraded.Status)
+	}
+	if downgraded.XMLID == nil {
+		t.Errorf("Incorrect XMLID; want: '%s', got: <nil>", xmlid)
+	} else if *downgraded.XMLID != xmlid {
+		t.Errorf("Incorrect XMLID; want: '%s', got: '%s'", xmlid, *downgraded.XMLID)
+	}
+}
+
+func ExampleDeliveryServiceRequestV40_SetXMLID() {
+	var dsr DeliveryServiceRequestV40
+	fmt.Println(dsr.XMLID == "")
+
+	dsr.DeliveryService = new(DeliveryServiceV4)
+	dsr.DeliveryService.XMLID = new(string)
+	*dsr.DeliveryService.XMLID = "test"
+	dsr.SetXMLID()
+
+	fmt.Println(dsr.XMLID)
+
+	// Output: true
+	// test
+}
diff --git a/lib/go-tc/time.go b/lib/go-tc/time.go
index f2a9424..f251c72 100644
--- a/lib/go-tc/time.go
+++ b/lib/go-tc/time.go
@@ -99,11 +99,18 @@ func (t *Time) UnmarshalJSON(b []byte) (err error) {
 // TimeNoMod supported JSON marshalling, but suppresses JSON unmarshalling
 type TimeNoMod Time
 
-// NewTimeNoMod returns the address of a TimeNoMod.
+// NewTimeNoMod returns the address of a TimeNoMod with a Time value of the
+// current time.
 func NewTimeNoMod() *TimeNoMod {
 	return &TimeNoMod{Time: time.Now()}
 }
 
+// TimeNoModFromTime returns a reference to a TimeNoMod with the given Time
+// value.
+func TimeNoModFromTime(t time.Time) *TimeNoMod {
+	return &TimeNoMod{Time: t}
+}
+
 // Scan implements the database/sql Scanner interface.
 func (t *TimeNoMod) Scan(value interface{}) error {
 	t.Time, t.Valid = value.(time.Time)
diff --git a/traffic_ops/testing/api/v1/deliveryservice_requests_test.go b/traffic_ops/testing/api/v1/deliveryservice_requests_test.go
index fafe978..aa10b2e 100644
--- a/traffic_ops/testing/api/v1/deliveryservice_requests_test.go
+++ b/traffic_ops/testing/api/v1/deliveryservice_requests_test.go
@@ -20,7 +20,6 @@ import (
 	"testing"
 
 	tc "github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/traffic_ops/testing/api/utils"
 )
 
 const (
@@ -94,12 +93,19 @@ func TestDeliveryServiceRequestTypeFields(t *testing.T) {
 			t.Errorf("Error occurred %v", err)
 		}
 
-		expected := []string{
-			"deliveryservice_request was created.",
-			//"'xmlId' the length must be between 1 and 48",
+		found := false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.ErrorLevel.String() {
+				t.Errorf("Expected only succuss-level alerts creating a DSR, got error-level alert: %s", alert.Text)
+			} else if alert.Level == tc.SuccessLevel.String() {
+				t.Logf("Got expected alert creating a DSR: %s", alert.Text)
+				found = true
+			}
 		}
 
-		utils.Compare(t, expected, alerts.ToStrings())
+		if !found {
+			t.Errorf("Expected a success-level alert creating a DSR, got none")
+		}
 
 		dsrs, _, err := TOSession.GetDeliveryServiceRequestByXMLID(dsr.DeliveryService.XMLID)
 		if len(dsrs) != 1 {
@@ -126,10 +132,19 @@ func TestDeliveryServiceRequestBad(t *testing.T) {
 		if err != nil {
 			t.Errorf("Error creating DeliveryServiceRequest %v", err)
 		}
-		expected := []string{
-			`'status' invalid transition from draft to pending`,
+		found := false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.SuccessLevel.String() {
+				t.Errorf("Expected only error-level alerts creating a DSR with a bad status, got success-level alert: %s", alert.Text)
+			} else if alert.Level == tc.ErrorLevel.String() {
+				t.Logf("Got expected alert creating a DSR with a bad status: %s", alert.Text)
+				found = true
+			}
+		}
+
+		if !found {
+			t.Errorf("Expected an error-level alert creating a DSR with a bad status, got none")
 		}
-		utils.Compare(t, expected, alerts.ToStrings())
 	})
 }
 
@@ -156,8 +171,18 @@ func TestDeliveryServiceRequestWorkflow(t *testing.T) {
 			t.Errorf("Error creating DeliveryServiceRequest %v", err)
 		}
 
-		expected := []string{`deliveryservice_request was created.`}
-		utils.Compare(t, expected, alerts.ToStrings())
+		found := false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.ErrorLevel.String() {
+				t.Errorf("Expected only succuss-level alerts creating a DSR, got error-level alert: %s", alert.Text)
+			} else if alert.Level == tc.SuccessLevel.String() {
+				t.Logf("Got expected alert creating a DSR: %s", alert.Text)
+				found = true
+			}
+		}
+		if !found {
+			t.Errorf("Expected a success-level alert creating a DSR, got none")
+		}
 
 		// Create a duplicate request -- should fail because xmlId is the same
 		alerts, _, err = TOSession.CreateDeliveryServiceRequest(src)
@@ -165,8 +190,18 @@ func TestDeliveryServiceRequestWorkflow(t *testing.T) {
 			t.Errorf("Error creating DeliveryServiceRequest %v", err)
 		}
 
-		expected = []string{`An active request exists for delivery service 'test-transitions'`}
-		utils.Compare(t, expected, alerts.ToStrings())
+		found = false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.SuccessLevel.String() {
+				t.Errorf("Expected only error-level alerts creating a duplicate DSR, got success-level alert: %s", alert.Text)
+			} else if alert.Level == tc.ErrorLevel.String() {
+				t.Logf("Got expected alert creating a duplicate DSR: %s", alert.Text)
+				found = true
+			}
+		}
+		if !found {
+			t.Errorf("Expected an error-level alert creating a DSR, got none")
+		}
 
 		dsrs, _, err = TOSession.GetDeliveryServiceRequestByXMLID(`test-transitions`)
 		if len(dsrs) != 1 {
@@ -178,11 +213,19 @@ func TestDeliveryServiceRequestWorkflow(t *testing.T) {
 
 		alerts, dsr := updateDeliveryServiceRequestStatus(t, dsrs[0], "submitted")
 
-		expected = []string{
-			"deliveryservice_request was updated.",
+		found = false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.ErrorLevel.String() {
+				t.Errorf("Expected only succuss-level alerts updating a DSR, got error-level alert: %s", alert.Text)
+			} else if alert.Level == tc.SuccessLevel.String() {
+				t.Logf("Got expected alert updating a DSR: %s", alert.Text)
+				found = true
+			}
+		}
+		if !found {
+			t.Errorf("Expected a success-level alert updating a DSR, got none")
 		}
 
-		utils.Compare(t, expected, alerts.ToStrings())
 		if dsr.Status != tc.RequestStatus("submitted") {
 			t.Errorf("expected status=submitted,  got %s", string(dsr.Status))
 		}
diff --git a/traffic_ops/testing/api/v2/deliveryservice_requests_test.go b/traffic_ops/testing/api/v2/deliveryservice_requests_test.go
index d1e0c9d..0025792 100644
--- a/traffic_ops/testing/api/v2/deliveryservice_requests_test.go
+++ b/traffic_ops/testing/api/v2/deliveryservice_requests_test.go
@@ -20,7 +20,6 @@ import (
 	"testing"
 
 	tc "github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/traffic_ops/testing/api/utils"
 )
 
 const (
@@ -94,12 +93,18 @@ func TestDeliveryServiceRequestTypeFields(t *testing.T) {
 			t.Errorf("Error occurred %v", err)
 		}
 
-		expected := []string{
-			"deliveryservice_request was created.",
-			//"'xmlId' the length must be between 1 and 48",
+		found := false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.ErrorLevel.String() {
+				t.Errorf("Expected only succuss-level alerts creating a DSR, got error-level alert: %s", alert.Text)
+			} else if alert.Level == tc.SuccessLevel.String() {
+				t.Logf("Got expected alert creating a DSR: %s", alert.Text)
+				found = true
+			}
+		}
+		if !found {
+			t.Errorf("Expected a success-level alert creating a DSR, got none")
 		}
-
-		utils.Compare(t, expected, alerts.ToStrings())
 
 		dsrs, _, err := TOSession.GetDeliveryServiceRequestByXMLID(dsr.DeliveryService.XMLID)
 		if len(dsrs) != 1 {
@@ -126,10 +131,20 @@ func TestDeliveryServiceRequestBad(t *testing.T) {
 		if err != nil {
 			t.Errorf("Error creating DeliveryServiceRequest %v", err)
 		}
-		expected := []string{
-			`'status' invalid transition from draft to pending`,
+
+		found := false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.SuccessLevel.String() {
+				t.Errorf("Expected only error-level alerts creating a DSR with a bad status, got success-level alert: %s", alert.Text)
+			} else if alert.Level == tc.ErrorLevel.String() {
+				t.Logf("Got expected alert creating a DSR with a bad status: %s", alert.Text)
+				found = true
+			}
+		}
+
+		if !found {
+			t.Errorf("Expected an error-level alert creating a DSR with a bad status, got none")
 		}
-		utils.Compare(t, expected, alerts.ToStrings())
 	})
 }
 
@@ -156,8 +171,18 @@ func TestDeliveryServiceRequestWorkflow(t *testing.T) {
 			t.Errorf("Error creating DeliveryServiceRequest %v", err)
 		}
 
-		expected := []string{`deliveryservice_request was created.`}
-		utils.Compare(t, expected, alerts.ToStrings())
+		found := false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.ErrorLevel.String() {
+				t.Errorf("Expected only succuss-level alerts creating a DSR, got error-level alert: %s", alert.Text)
+			} else if alert.Level == tc.SuccessLevel.String() {
+				t.Logf("Got expected alert creating a DSR: %s", alert.Text)
+				found = true
+			}
+		}
+		if !found {
+			t.Errorf("Expected a success-level alert creating a DSR, got none")
+		}
 
 		// Create a duplicate request -- should fail because xmlId is the same
 		alerts, _, err = TOSession.CreateDeliveryServiceRequest(src)
@@ -165,8 +190,18 @@ func TestDeliveryServiceRequestWorkflow(t *testing.T) {
 			t.Errorf("Error creating DeliveryServiceRequest %v", err)
 		}
 
-		expected = []string{`An active request exists for delivery service 'test-transitions'`}
-		utils.Compare(t, expected, alerts.ToStrings())
+		found = false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.SuccessLevel.String() {
+				t.Errorf("Expected only error-level alerts creating a duplicate DSR, got success-level alert: %s", alert.Text)
+			} else if alert.Level == tc.ErrorLevel.String() {
+				t.Logf("Got expected alert creating a duplicate DSR: %s", alert.Text)
+				found = true
+			}
+		}
+		if !found {
+			t.Errorf("Expected an error-level alert creating a DSR, got none")
+		}
 
 		dsrs, _, err = TOSession.GetDeliveryServiceRequestByXMLID(`test-transitions`)
 		if len(dsrs) != 1 {
@@ -178,11 +213,19 @@ func TestDeliveryServiceRequestWorkflow(t *testing.T) {
 
 		alerts, dsr := updateDeliveryServiceRequestStatus(t, dsrs[0], "submitted")
 
-		expected = []string{
-			"deliveryservice_request was updated.",
+		found = false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.ErrorLevel.String() {
+				t.Errorf("Expected only succuss-level alerts updating a DSR, got error-level alert: %s", alert.Text)
+			} else if alert.Level == tc.SuccessLevel.String() {
+				t.Logf("Got expected alert updating a DSR: %s", alert.Text)
+				found = true
+			}
+		}
+		if !found {
+			t.Errorf("Expected a success-level alert updating a DSR, got none")
 		}
 
-		utils.Compare(t, expected, alerts.ToStrings())
 		if dsr.Status != tc.RequestStatus("submitted") {
 			t.Errorf("expected status=submitted,  got %s", string(dsr.Status))
 		}
diff --git a/traffic_ops/testing/api/v3/deliveryservice_requests_test.go b/traffic_ops/testing/api/v3/deliveryservice_requests_test.go
index 8b8ecf8..11cc733 100644
--- a/traffic_ops/testing/api/v3/deliveryservice_requests_test.go
+++ b/traffic_ops/testing/api/v3/deliveryservice_requests_test.go
@@ -23,7 +23,6 @@ import (
 
 	"github.com/apache/trafficcontrol/lib/go-rfc"
 	tc "github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/traffic_ops/testing/api/utils"
 )
 
 const (
@@ -147,12 +146,18 @@ func TestDeliveryServiceRequestTypeFields(t *testing.T) {
 			t.Errorf("Error occurred %v", err)
 		}
 
-		expected := []string{
-			"deliveryservice_request was created.",
-			//"'xmlId' the length must be between 1 and 48",
+		found := false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.ErrorLevel.String() {
+				t.Errorf("Expected only succuss-level alerts creating a DSR, got error-level alert: %s", alert.Text)
+			} else if alert.Level == tc.SuccessLevel.String() {
+				t.Logf("Got expected alert creating a DSR: %s", alert.Text)
+				found = true
+			}
+		}
+		if !found {
+			t.Errorf("Expected a success-level alert creating a DSR, got none")
 		}
-
-		utils.Compare(t, expected, alerts.ToStrings())
 
 		dsrs, _, err := TOSession.GetDeliveryServiceRequestByXMLID(dsr.DeliveryService.XMLID)
 		if len(dsrs) != 1 {
@@ -179,10 +184,6 @@ func TestDeliveryServiceRequestBad(t *testing.T) {
 		if err == nil {
 			t.Fatal("expected: validation error, actual: nil")
 		}
-		expected := `'status' invalid transition from draft to pending`
-		if !strings.Contains(err.Error(), expected) {
-			t.Fatalf("expected: error message to contain %s, actual: %s", expected, err.Error())
-		}
 	})
 }
 
@@ -209,8 +210,18 @@ func TestDeliveryServiceRequestWorkflow(t *testing.T) {
 			t.Errorf("Error creating DeliveryServiceRequest %v", err)
 		}
 
-		expected := []string{`deliveryservice_request was created.`}
-		utils.Compare(t, expected, alerts.ToStrings())
+		found := false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.ErrorLevel.String() {
+				t.Errorf("Expected only succuss-level alerts creating a DSR, got error-level alert: %s", alert.Text)
+			} else if alert.Level == tc.SuccessLevel.String() {
+				t.Logf("Got expected alert creating a DSR: %s", alert.Text)
+				found = true
+			}
+		}
+		if !found {
+			t.Errorf("Expected a success-level alert creating a DSR, got none")
+		}
 
 		// Create a duplicate request -- should fail because xmlId is the same
 		alerts, _, err = TOSession.CreateDeliveryServiceRequest(src)
@@ -218,11 +229,21 @@ func TestDeliveryServiceRequestWorkflow(t *testing.T) {
 			t.Fatal("expected: validation error, actual: nil")
 		}
 
-		expectedStr := `An active request exists for delivery service 'test-transitions'`
-		if !strings.Contains(err.Error(), expectedStr) {
-			t.Errorf("expected: error message containing %s, actual: %v", expectedStr, err)
-		}
-		utils.Compare(t, expected, alerts.ToStrings())
+		// TODO: the client needs to be fixed to return alerts on error. But
+		// that's gotten harder to do "semantically" now that most of the logic
+		// is shared between all API versions.
+		// found = false
+		// for _, alert := range alerts.Alerts {
+		// 	if alert.Level == tc.SuccessLevel.String() {
+		// 		t.Errorf("Expected only error-level alerts creating a duplicate DSR, got success-level alert: %s", alert.Text)
+		// 	} else if alert.Level == tc.ErrorLevel.String() {
+		// 		t.Logf("Got expected alert creating a duplicate DSR: %s", alert.Text)
+		// 		found = true
+		// 	}
+		// }
+		// if !found {
+		// 	t.Errorf("Expected an error-level alert creating a duplicate DSR, got none")
+		// }
 
 		dsrs, _, err = TOSession.GetDeliveryServiceRequestByXMLID(`test-transitions`)
 		if len(dsrs) != 1 {
@@ -234,11 +255,19 @@ func TestDeliveryServiceRequestWorkflow(t *testing.T) {
 
 		alerts, dsr := updateDeliveryServiceRequestStatus(t, dsrs[0], "submitted", nil)
 
-		expected = []string{
-			"deliveryservice_request was updated.",
+		found = false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.ErrorLevel.String() {
+				t.Errorf("Expected only succuss-level alerts updating a DSR, got error-level alert: %s", alert.Text)
+			} else if alert.Level == tc.SuccessLevel.String() {
+				t.Logf("Got expected alert updating a DSR: %s", alert.Text)
+				found = true
+			}
+		}
+		if !found {
+			t.Errorf("Expected a success-level alert updating a DSR, got none")
 		}
 
-		utils.Compare(t, expected, alerts.ToStrings())
 		if dsr.Status != tc.RequestStatus("submitted") {
 			t.Errorf("expected status=submitted,  got %s", string(dsr.Status))
 		}
diff --git a/traffic_ops/testing/api/v4/deliveryservice_request_comments_test.go b/traffic_ops/testing/api/v4/deliveryservice_request_comments_test.go
index 2a3b0d2..e2ad209 100644
--- a/traffic_ops/testing/api/v4/deliveryservice_request_comments_test.go
+++ b/traffic_ops/testing/api/v4/deliveryservice_request_comments_test.go
@@ -89,25 +89,30 @@ func CreateTestDeliveryServiceRequestComments(t *testing.T) {
 
 	// Retrieve a delivery service request by xmlId so we can get the ID needed to create a dsr comment
 	dsr := testData.DeliveryServiceRequests[0].DeliveryService
+	resetDS(dsr)
+	if dsr == nil || dsr.XMLID == nil {
+		t.Fatal("first DSR in the test data had a nil DeliveryService or one with no XMLID")
+	}
 
-	resp, _, err := TOSession.GetDeliveryServiceRequestByXMLID(dsr.XMLID)
+	resp, _, _, err := TOSession.GetDeliveryServiceRequestsByXMLID(*dsr.XMLID, nil)
 	if err != nil {
-		t.Errorf("cannot GET delivery service request by xml id: %v - %v", dsr.XMLID, err)
+		t.Fatalf("cannot GET delivery service request by xml id: %v - %v", dsr.XMLID, err)
 	}
 	if len(resp) != 1 {
-		t.Errorf("found %d delivery service request by xml id, expected %d: %s", len(resp), 1, dsr.XMLID)
-	} else {
-		respDSR := resp[0]
-
-		for _, comment := range testData.DeliveryServiceRequestComments {
-			comment.DeliveryServiceRequestID = respDSR.ID
-			resp, _, err := TOSession.CreateDeliveryServiceRequestComment(comment)
-			if err != nil {
-				t.Errorf("could not CREATE delivery service request comment: %v - %v", err, resp)
-			}
-		}
+		t.Fatalf("found %d delivery service request by xml id, expected %d: %s", len(resp), 1, *dsr.XMLID)
+	}
+	respDSR := resp[0]
+	if respDSR.ID == nil {
+		t.Fatalf("got Delivery Service Request with xml_id '%s' that had a null ID", *dsr.XMLID)
 	}
 
+	for _, comment := range testData.DeliveryServiceRequestComments {
+		comment.DeliveryServiceRequestID = *respDSR.ID
+		resp, _, err := TOSession.CreateDeliveryServiceRequestComment(comment)
+		if err != nil {
+			t.Errorf("could not CREATE delivery service request comment: %v - %v", err, resp)
+		}
+	}
 }
 
 func SortTestDeliveryServiceRequestComments(t *testing.T) {
diff --git a/traffic_ops/testing/api/v4/deliveryservice_requests_test.go b/traffic_ops/testing/api/v4/deliveryservice_requests_test.go
index 1960d9d..5ec37fa 100644
--- a/traffic_ops/testing/api/v4/deliveryservice_requests_test.go
+++ b/traffic_ops/testing/api/v4/deliveryservice_requests_test.go
@@ -23,7 +23,6 @@ import (
 
 	"github.com/apache/trafficcontrol/lib/go-rfc"
 	tc "github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/traffic_ops/testing/api/utils"
 )
 
 const (
@@ -33,6 +32,27 @@ const (
 	dsrDraft     = 3
 )
 
+// this resets the IDs of things attached to a DS, which needs to be done
+// because the WithObjs flow destroys and recreates those object IDs
+// non-deterministically with each test - BUT, the client method permanently
+// alters the DSR structures by adding these referential IDs. Older clients
+// got away with it by not making 'DeliveryService' a pointer, but to add
+// original/requested fields you need to sometimes allow each to be nil, so
+// this is a problem that needs to be solved at some point.
+// A better solution _might_ be to reload all the test fixtures every time
+// to wipe any and all referential modifications made to any test data, but
+// for now that's overkill.
+func resetDS(ds *tc.DeliveryServiceV4) {
+	if ds == nil {
+		return
+	}
+	ds.CDNID = nil
+	ds.ID = nil
+	ds.ProfileID = nil
+	ds.TenantID = nil
+	ds.TypeID = nil
+}
+
 func TestDeliveryServiceRequests(t *testing.T) {
 	WithObjs(t, []TCObj{CDNs, Types, Parameters, Tenants, DeliveryServiceRequests}, func() {
 		GetTestDeliveryServiceRequestsIMS(t)
@@ -56,7 +76,11 @@ func TestDeliveryServiceRequests(t *testing.T) {
 func UpdateTestDeliveryServiceRequestsWithHeaders(t *testing.T, header http.Header) {
 	// Retrieve the DeliveryServiceRequest by name so we can get the id for the Update
 	dsr := testData.DeliveryServiceRequests[dsrGood]
-	resp, _, err := TOSession.GetDeliveryServiceRequestByXMLIDWithHdr(dsr.DeliveryService.XMLID, header)
+	resetDS(dsr.DeliveryService)
+	if dsr.DeliveryService == nil || dsr.DeliveryService.XMLID == nil {
+		t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrGood)
+	}
+	resp, _, _, err := TOSession.GetDeliveryServiceRequestsByXMLID(*dsr.DeliveryService.XMLID, header)
 	if err != nil {
 		t.Errorf("cannot GET DeliveryServiceRequest by name: %v - %v", dsr.DeliveryService.XMLID, err)
 	}
@@ -64,9 +88,14 @@ func UpdateTestDeliveryServiceRequestsWithHeaders(t *testing.T, header http.Head
 		t.Fatal("Length of GET DeliveryServiceRequest is 0")
 	}
 	respDSR := resp[0]
-	respDSR.DeliveryService.DisplayName = "new display name"
+	if respDSR.ID == nil {
+		t.Fatalf("Got a DSR for XML ID '%s' that had a nil ID", *dsr.DeliveryService.XMLID)
+	}
+
+	respDSR.DeliveryService.DisplayName = new(string)
+	*respDSR.DeliveryService.DisplayName = "new display name"
 
-	_, reqInf, err := TOSession.UpdateDeliveryServiceRequestByIDWithHdr(respDSR.ID, respDSR, header)
+	_, _, reqInf, err := TOSession.UpdateDeliveryServiceRequest(*respDSR.ID, respDSR, header)
 	if err == nil {
 		t.Errorf("Expected error about precondition failed, but got none")
 	}
@@ -77,7 +106,11 @@ func UpdateTestDeliveryServiceRequestsWithHeaders(t *testing.T, header http.Head
 
 func GetTestDeliveryServiceRequestsIMSAfterChange(t *testing.T, header http.Header) {
 	dsr := testData.DeliveryServiceRequests[dsrGood]
-	_, reqInf, err := TOSession.GetDeliveryServiceRequestByXMLIDWithHdr(dsr.DeliveryService.XMLID, header)
+	resetDS(dsr.DeliveryService)
+	if dsr.DeliveryService == nil || dsr.DeliveryService.XMLID == nil {
+		t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrGood)
+	}
+	_, _, reqInf, err := TOSession.GetDeliveryServiceRequestsByXMLID(*dsr.DeliveryService.XMLID, header)
 	if err != nil {
 		t.Fatalf("Expected no error, but got %v", err.Error())
 	}
@@ -88,7 +121,7 @@ func GetTestDeliveryServiceRequestsIMSAfterChange(t *testing.T, header http.Head
 	currentTime = currentTime.Add(1 * time.Second)
 	timeStr := currentTime.Format(time.RFC1123)
 	header.Set(rfc.IfModifiedSince, timeStr)
-	_, reqInf, err = TOSession.GetDeliveryServiceRequestByXMLIDWithHdr(dsr.DeliveryService.XMLID, header)
+	_, _, reqInf, err = TOSession.GetDeliveryServiceRequestsByXMLID(*dsr.DeliveryService.XMLID, header)
 	if err != nil {
 		t.Fatalf("Expected no error, but got %v", err.Error())
 	}
@@ -101,8 +134,10 @@ func CreateTestDeliveryServiceRequests(t *testing.T) {
 	t.Log("CreateTestDeliveryServiceRequests")
 
 	dsr := testData.DeliveryServiceRequests[dsrGood]
-	respDSR, _, err := TOSession.CreateDeliveryServiceRequest(dsr)
+	resetDS(dsr.DeliveryService)
+	respDSR, alerts, _, err := TOSession.CreateDeliveryServiceRequest(dsr, nil)
 	t.Log("Response: ", respDSR)
+	t.Logf("Alerts from creating a dsr: %+v", alerts)
 	if err != nil {
 		t.Errorf("could not CREATE DeliveryServiceRequests: %v", err)
 	}
@@ -112,7 +147,8 @@ func CreateTestDeliveryServiceRequests(t *testing.T) {
 func TestDeliveryServiceRequestRequired(t *testing.T) {
 	WithObjs(t, []TCObj{CDNs, Types, Parameters, Tenants}, func() {
 		dsr := testData.DeliveryServiceRequests[dsrRequired]
-		_, _, err := TOSession.CreateDeliveryServiceRequest(dsr)
+		resetDS(dsr.DeliveryService)
+		_, _, _, err := TOSession.CreateDeliveryServiceRequest(dsr, nil)
 		if err == nil {
 			t.Error("expected: validation error, actual: nil")
 		}
@@ -127,11 +163,15 @@ func TestDeliveryServiceRequestRules(t *testing.T) {
 		displayName := strings.Repeat("X", 49)
 
 		dsr := testData.DeliveryServiceRequests[dsrGood]
-		dsr.DeliveryService.DisplayName = displayName
-		dsr.DeliveryService.RoutingName = routingName
-		dsr.DeliveryService.XMLID = XMLID
+		resetDS(dsr.DeliveryService)
+		if dsr.DeliveryService == nil {
+			t.Fatalf("the %dth DSR in the test data had no DeliveryService", dsrGood)
+		}
+		dsr.DeliveryService.DisplayName = &displayName
+		dsr.DeliveryService.RoutingName = &routingName
+		dsr.DeliveryService.XMLID = &XMLID
 
-		_, _, err := TOSession.CreateDeliveryServiceRequest(dsr)
+		_, _, _, err := TOSession.CreateDeliveryServiceRequest(dsr, nil)
 		if err == nil {
 			t.Error("expected: validation error, actual: nil")
 		}
@@ -141,24 +181,39 @@ func TestDeliveryServiceRequestRules(t *testing.T) {
 func TestDeliveryServiceRequestTypeFields(t *testing.T) {
 	WithObjs(t, []TCObj{CDNs, Types, Tenants, Parameters}, func() {
 		dsr := testData.DeliveryServiceRequests[dsrBadTenant]
+		resetDS(dsr.DeliveryService)
+		if dsr.DeliveryService == nil || dsr.DeliveryService.XMLID == nil {
+			t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrBadTenant)
+		}
 
-		alerts, _, err := TOSession.CreateDeliveryServiceRequest(dsr)
+		_, alerts, _, err := TOSession.CreateDeliveryServiceRequest(dsr, nil)
 		if err != nil {
 			t.Errorf("Error occurred %v", err)
 		}
 
-		expected := []string{
-			"deliveryservice_request was created.",
-			//"'xmlId' the length must be between 1 and 48",
+		found := false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.ErrorLevel.String() {
+				t.Errorf("Expected only succuss-level alerts creating a DSR, got error-level alert: %s", alert.Text)
+			} else if alert.Level == tc.SuccessLevel.String() {
+				t.Logf("Got expected alert creating a DSR: %s", alert.Text)
+				found = true
+			}
 		}
 
-		utils.Compare(t, expected, alerts.ToStrings())
+		if !found {
+			t.Errorf("Expected a success-level alert creating a DSR, got none")
+		}
 
-		dsrs, _, err := TOSession.GetDeliveryServiceRequestByXMLID(dsr.DeliveryService.XMLID)
+		dsrs, _, _, err := TOSession.GetDeliveryServiceRequestsByXMLID(*dsr.DeliveryService.XMLID, nil)
 		if len(dsrs) != 1 {
-			t.Errorf("expected 1 deliveryservice_request with XMLID %s;  got %d", dsr.DeliveryService.XMLID, len(dsrs))
+			t.Fatalf("expected 1 deliveryservice_request with XMLID %s;  got %d", *dsr.DeliveryService.XMLID, len(dsrs))
+		}
+		if dsrs[0].ID == nil {
+			t.Fatalf("got a DSR with a null ID by XMLID '%s'", *dsr.DeliveryService.XMLID)
 		}
-		alert, _, err := TOSession.DeleteDeliveryServiceRequestByID(dsrs[0].ID)
+
+		alert, _, err := TOSession.DeleteDeliveryServiceRequest(*dsrs[0].ID)
 		if err != nil {
 			t.Errorf("cannot DELETE DeliveryServiceRequest by id: %d - %v - %v", dsrs[0].ID, err, alert)
 		}
@@ -169,20 +224,17 @@ func TestDeliveryServiceRequestBad(t *testing.T) {
 	WithObjs(t, []TCObj{CDNs, Types, Parameters, Tenants}, func() {
 		// try to create non-draft/submitted
 		src := testData.DeliveryServiceRequests[dsrDraft]
+		resetDS(src.DeliveryService)
 		s, err := tc.RequestStatusFromString("pending")
 		if err != nil {
 			t.Errorf(`unable to create Status from string "pending"`)
 		}
 		src.Status = s
 
-		_, _, err = TOSession.CreateDeliveryServiceRequest(src)
+		_, _, _, err = TOSession.CreateDeliveryServiceRequest(src, nil)
 		if err == nil {
 			t.Fatal("expected: validation error, actual: nil")
 		}
-		expected := `'status' invalid transition from draft to pending`
-		if !strings.Contains(err.Error(), expected) {
-			t.Fatalf("expected: error message to contain %s, actual: %s", expected, err.Error())
-		}
 	})
 }
 
@@ -190,7 +242,7 @@ func TestDeliveryServiceRequestBad(t *testing.T) {
 func TestDeliveryServiceRequestWorkflow(t *testing.T) {
 	WithObjs(t, []TCObj{CDNs, Types, Parameters, Tenants}, func() {
 		// test empty request table
-		dsrs, _, err := TOSession.GetDeliveryServiceRequests()
+		dsrs, _, _, err := TOSession.GetDeliveryServiceRequests(nil)
 		if err != nil {
 			t.Errorf("Error getting empty list of DeliveryServiceRequests %v++", err)
 		}
@@ -203,28 +255,50 @@ func TestDeliveryServiceRequestWorkflow(t *testing.T) {
 
 		// Create a draft request
 		src := testData.DeliveryServiceRequests[dsrDraft]
+		resetDS(src.DeliveryService)
 
-		alerts, _, err := TOSession.CreateDeliveryServiceRequest(src)
+		_, alerts, _, err := TOSession.CreateDeliveryServiceRequest(src, nil)
 		if err != nil {
 			t.Errorf("Error creating DeliveryServiceRequest %v", err)
 		}
 
-		expected := []string{`deliveryservice_request was created.`}
-		utils.Compare(t, expected, alerts.ToStrings())
+		found := false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.ErrorLevel.String() {
+				t.Errorf("Expected only succuss-level alerts creating a DSR, got error-level alert: %s", alert.Text)
+			} else if alert.Level == tc.SuccessLevel.String() {
+				t.Logf("Got expected alert creating a DSR: %s", alert.Text)
+				found = true
+			}
+		}
+
+		if !found {
+			t.Errorf("Expected a success-level alert creating a DSR, got none")
+		}
 
 		// Create a duplicate request -- should fail because xmlId is the same
-		alerts, _, err = TOSession.CreateDeliveryServiceRequest(src)
+		_, alerts, _, err = TOSession.CreateDeliveryServiceRequest(src, nil)
 		if err == nil {
 			t.Fatal("expected: validation error, actual: nil")
 		}
 
-		expectedStr := `An active request exists for delivery service 'test-transitions'`
-		if !strings.Contains(err.Error(), expectedStr) {
-			t.Errorf("expected: error message containing %s, actual: %v", expectedStr, err)
-		}
-		utils.Compare(t, expected, alerts.ToStrings())
-
-		dsrs, _, err = TOSession.GetDeliveryServiceRequestByXMLID(`test-transitions`)
+		// TODO: the client needs to be fixed to return alerts on error. But
+		// that's gotten harder to do "semantically" now that most of the logic
+		// is shared between all API versions.
+		// found = false
+		// for _, alert := range alerts.Alerts {
+		// 	if alert.Level == tc.SuccessLevel.String() {
+		// 		t.Errorf("Expected only error-level alerts creating a duplicate DSR, got success-level alert: %s", alert.Text)
+		// 	} else if alert.Level == tc.ErrorLevel.String() {
+		// 		t.Logf("Got expected alert creating a duplicate DSR: %s", alert.Text)
+		// 		found = true
+		// 	}
+		// }
+		// if !found {
+		// 	t.Errorf("Expected an error-level alert creating a duplicate DSR, got none")
+		// }
+
+		dsrs, _, _, err = TOSession.GetDeliveryServiceRequestsByXMLID(`test-transitions`, nil)
 		if len(dsrs) != 1 {
 			t.Errorf("Expected 1 deliveryServiceRequest -- got %d", len(dsrs))
 			if len(dsrs) == 0 {
@@ -234,37 +308,46 @@ func TestDeliveryServiceRequestWorkflow(t *testing.T) {
 
 		alerts, dsr := updateDeliveryServiceRequestStatus(t, dsrs[0], "submitted", nil)
 
-		expected = []string{
-			"deliveryservice_request was updated.",
+		found = false
+		for _, alert := range alerts.Alerts {
+			if alert.Level == tc.ErrorLevel.String() {
+				t.Errorf("Expected only succuss-level alerts updating a DSR, got error-level alert: %s", alert.Text)
+			} else if alert.Level == tc.SuccessLevel.String() {
+				t.Logf("Got expected alert updating a DSR: %s", alert.Text)
+				found = true
+			}
+		}
+		if !found {
+			t.Errorf("Expected a success-level alert updating a DSR, got none")
 		}
 
-		utils.Compare(t, expected, alerts.ToStrings())
 		if dsr.Status != tc.RequestStatus("submitted") {
 			t.Errorf("expected status=submitted,  got %s", string(dsr.Status))
 		}
 	})
 }
 
-func updateDeliveryServiceRequestStatus(t *testing.T, dsr tc.DeliveryServiceRequest, newstate string, header http.Header) (tc.Alerts, tc.DeliveryServiceRequest) {
+func updateDeliveryServiceRequestStatus(t *testing.T, dsr tc.DeliveryServiceRequestV40, newstate string, header http.Header) (tc.Alerts, tc.DeliveryServiceRequestV40) {
 	ID := dsr.ID
+	if ID == nil {
+		t.Error("updateDeliveryServiceRequestStatus called with a DSR that has a nil ID")
+		return tc.Alerts{}, tc.DeliveryServiceRequestV40{}
+	}
 	dsr.Status = tc.RequestStatus("submitted")
 
-	alerts, _, err := TOSession.UpdateDeliveryServiceRequestByIDWithHdr(ID, dsr, header)
+	_, alerts, _, err := TOSession.UpdateDeliveryServiceRequest(*ID, dsr, header)
 	if err != nil {
 		t.Errorf("Error updating deliveryservice_request: %v", err)
 		return alerts, dsr
 	}
 
-	d, _, err := TOSession.GetDeliveryServiceRequestByID(ID)
+	d, _, _, err := TOSession.GetDeliveryServiceRequest(*ID, nil)
 	if err != nil {
 		t.Errorf("Error updating deliveryservice_request %d: %v", ID, err)
 		return alerts, dsr
 	}
 
-	if len(d) != 1 {
-		t.Errorf("Expected 1 deliveryservice_request, got %d", len(d))
-	}
-	return alerts, d[0]
+	return alerts, d
 }
 
 func GetTestDeliveryServiceRequestsIMS(t *testing.T) {
@@ -274,7 +357,12 @@ func GetTestDeliveryServiceRequestsIMS(t *testing.T) {
 	time := futureTime.Format(time.RFC1123)
 	header.Set(rfc.IfModifiedSince, time)
 	dsr := testData.DeliveryServiceRequests[dsrGood]
-	_, reqInf, err := TOSession.GetDeliveryServiceRequestByXMLIDWithHdr(dsr.DeliveryService.XMLID, header)
+	resetDS(dsr.DeliveryService)
+	if dsr.DeliveryService == nil || dsr.DeliveryService.XMLID == nil {
+		t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrGood)
+	}
+
+	_, _, reqInf, err := TOSession.GetDeliveryServiceRequestsByXMLID(*dsr.DeliveryService.XMLID, header)
 	if err != nil {
 		t.Fatalf("Expected no error, but got %v", err.Error())
 	}
@@ -285,7 +373,11 @@ func GetTestDeliveryServiceRequestsIMS(t *testing.T) {
 
 func GetTestDeliveryServiceRequests(t *testing.T) {
 	dsr := testData.DeliveryServiceRequests[dsrGood]
-	resp, _, err := TOSession.GetDeliveryServiceRequestByXMLID(dsr.DeliveryService.XMLID)
+	resetDS(dsr.DeliveryService)
+	if dsr.DeliveryService == nil || dsr.DeliveryService.XMLID == nil {
+		t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrGood)
+	}
+	resp, _, _, err := TOSession.GetDeliveryServiceRequestsByXMLID(*dsr.DeliveryService.XMLID, nil)
 	if err != nil {
 		t.Errorf("cannot GET DeliveryServiceRequest by XMLID: %v - %v", err, resp)
 	}
@@ -295,7 +387,12 @@ func UpdateTestDeliveryServiceRequests(t *testing.T) {
 
 	// Retrieve the DeliveryServiceRequest by name so we can get the id for the Update
 	dsr := testData.DeliveryServiceRequests[dsrGood]
-	resp, _, err := TOSession.GetDeliveryServiceRequestByXMLID(dsr.DeliveryService.XMLID)
+	resetDS(dsr.DeliveryService)
+	if dsr.DeliveryService == nil || dsr.DeliveryService.XMLID == nil {
+		t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrGood)
+	}
+
+	resp, _, _, err := TOSession.GetDeliveryServiceRequestsByXMLID(*dsr.DeliveryService.XMLID, nil)
 	if err != nil {
 		t.Errorf("cannot GET DeliveryServiceRequest by name: %v - %v", dsr.DeliveryService.XMLID, err)
 	}
@@ -303,10 +400,14 @@ func UpdateTestDeliveryServiceRequests(t *testing.T) {
 		t.Fatal("Length of GET DeliveryServiceRequest is 0")
 	}
 	respDSR := resp[0]
+	if respDSR.ID == nil {
+		t.Fatalf("got a DSR by XMLID '%s' with a null or undefined ID", *dsr.DeliveryService.XMLID)
+	}
 	expDisplayName := "new display name"
-	respDSR.DeliveryService.DisplayName = expDisplayName
+	respDSR.DeliveryService.DisplayName = &expDisplayName
 	var alert tc.Alerts
-	alert, _, err = TOSession.UpdateDeliveryServiceRequestByID(respDSR.ID, respDSR)
+	id := *respDSR.ID
+	_, alert, _, err = TOSession.UpdateDeliveryServiceRequest(id, respDSR, nil)
 	t.Log("Response: ", alert)
 	if err != nil {
 		t.Errorf("cannot UPDATE DeliveryServiceRequest by id: %v - %v", err, alert)
@@ -314,39 +415,50 @@ func UpdateTestDeliveryServiceRequests(t *testing.T) {
 	}
 
 	// Retrieve the DeliveryServiceRequest to check DeliveryServiceRequest name got updated
-	resp, _, err = TOSession.GetDeliveryServiceRequestByID(respDSR.ID)
+	respDSR, _, _, err = TOSession.GetDeliveryServiceRequest(id, nil)
 	if err != nil {
-		t.Errorf("cannot GET DeliveryServiceRequest by name: %v - %v", respDSR.ID, err)
-	} else {
-		respDSR = resp[0]
-		if respDSR.DeliveryService.DisplayName != expDisplayName {
-			t.Errorf("results do not match actual: %s, expected: %s", respDSR.DeliveryService.DisplayName, expDisplayName)
-		}
+		t.Fatalf("cannot GET DeliveryServiceRequest by ID: %v - %v", id, err)
+	}
+	if respDSR.DeliveryService == nil || respDSR.DeliveryService.DisplayName == nil {
+		t.Fatalf("Got DSR by ID '%d' that had no DeliveryService - or said DeliveryService had no DisplayName", *respDSR.ID)
+	}
+	if *respDSR.DeliveryService.DisplayName != expDisplayName {
+		t.Errorf("results do not match actual: %s, expected: %s", *respDSR.DeliveryService.DisplayName, expDisplayName)
 	}
-
 }
 
 func DeleteTestDeliveryServiceRequests(t *testing.T) {
 
 	// Retrieve the DeliveryServiceRequest by name so we can get the id for the Update
 	dsr := testData.DeliveryServiceRequests[dsrGood]
-	resp, _, err := TOSession.GetDeliveryServiceRequestByXMLID(dsr.DeliveryService.XMLID)
+	resetDS(dsr.DeliveryService)
+	if dsr.DeliveryService == nil || dsr.DeliveryService.XMLID == nil {
+		t.Fatalf("the %dth DSR in the test data had no DeliveryService - or that DeliveryService had no XMLID", dsrGood)
+	}
+
+	resp, _, _, err := TOSession.GetDeliveryServiceRequestsByXMLID(*dsr.DeliveryService.XMLID, nil)
 	if err != nil {
-		t.Errorf("cannot GET DeliveryServiceRequest by id: %v - %v", dsr.DeliveryService.XMLID, err)
+		t.Fatalf("cannot GET DeliveryServiceRequest by id: %v - %v", dsr.DeliveryService.XMLID, err)
+	}
+	if len(resp) < 1 {
+		t.Fatal("expected at least one Delivery Service Request, got none")
 	}
 	respDSR := resp[0]
-	alert, _, err := TOSession.DeleteDeliveryServiceRequestByID(respDSR.ID)
+	if respDSR.ID == nil {
+		t.Fatalf("Got a DSR by XMLID '%s' that had no ID", *dsr.DeliveryService.XMLID)
+	}
+	alert, _, err := TOSession.DeleteDeliveryServiceRequest(*respDSR.ID)
 	t.Log("Response: ", alert)
 	if err != nil {
 		t.Errorf("cannot DELETE DeliveryServiceRequest by id: %d - %v - %v", respDSR.ID, err, alert)
 	}
 
 	// Retrieve the DeliveryServiceRequest to see if it got deleted
-	dsrs, _, err := TOSession.GetDeliveryServiceRequestByXMLID(dsr.DeliveryService.XMLID)
+	dsrs, _, _, err := TOSession.GetDeliveryServiceRequestsByXMLID(*dsr.DeliveryService.XMLID, nil)
 	if err != nil {
 		t.Errorf("error deleting DeliveryServiceRequest name: %s", err.Error())
 	}
 	if len(dsrs) > 0 {
-		t.Errorf("expected DeliveryServiceRequest XMLID: %s to be deleted", dsr.DeliveryService.XMLID)
+		t.Errorf("expected DeliveryServiceRequest XMLID: %s to be deleted", *dsr.DeliveryService.XMLID)
 	}
 }
diff --git a/traffic_ops/testing/api/v4/tc-fixtures.json b/traffic_ops/testing/api/v4/tc-fixtures.json
index 7e90bd3..21ffdf2 100644
--- a/traffic_ops/testing/api/v4/tc-fixtures.json
+++ b/traffic_ops/testing/api/v4/tc-fixtures.json
@@ -305,8 +305,16 @@
                 "geoLimit": 1,
                 "geoProvider": 1,
                 "initialDispersion": 1,
+                "ipv6RoutingEnabled": true,
                 "logsEnabled": true,
                 "longDesc": "long desc",
+                "missLat": 0.0,
+                "missLong": 0.0,
+                "multiSiteOrigin": false,
+                "orgServerFqdn": "http://example.test",
+                "protocol": 0,
+                "qstringIgnore": 0,
+                "rangeRequestHandling": 0,
                 "regionalGeoBlocking": true,
                 "routingName": "goodroute",
                 "tenant": "tenant1",
@@ -327,8 +335,16 @@
                 "geoLimit": 0,
                 "geoProvider": 0,
                 "initialDispersion": 3,
+                "ipv6RoutingEnabled": true,
                 "logsEnabled": false,
                 "longDesc": "long desc",
+                "missLat": 0.0,
+                "missLong": 0.0,
+                "multiSiteOrigin": false,
+                "orgServerFqdn": "http://example.test",
+                "protocol": 0,
+                "qstringIgnore": 0,
+                "rangeRequestHandling": 0,
                 "regionalGeoBlocking": false,
                 "tenant": "root",
                 "type": "HTTP",
@@ -347,9 +363,16 @@
                 "geoProvider": 1,
                 "infoUrl": "xxx",
                 "initialDispersion": 1,
+                "ipv6RoutingEnabled": true,
                 "logsEnabled": true,
                 "longDesc": "long desc",
-                "orgServerFqdn": "xxx",
+                "missLat": 0.0,
+                "missLong": 0.0,
+                "multiSiteOrigin": false,
+                "orgServerFqdn": "http://example.test",
+                "protocol": 0,
+                "qstringIgnore": 0,
+                "rangeRequestHandling": 0,
                 "regionalGeoBlocking": true,
                 "routingName": "x routing",
                 "tenant": "tenant1",
@@ -370,8 +393,16 @@
                 "geoLimit": 1,
                 "geoProvider": 1,
                 "initialDispersion": 1,
+                "ipv6RoutingEnabled": true,
                 "logsEnabled": true,
                 "longDesc": "long desc",
+                "missLat": 0.0,
+                "missLong": 0.0,
+                "multiSiteOrigin": false,
+                "orgServerFqdn": "http://example.test",
+                "protocol": 0,
+                "qstringIgnore": 0,
+                "rangeRequestHandling": 0,
                 "regionalGeoBlocking": true,
                 "routingName": "goodroute",
                 "tenant": "tenant1",
diff --git a/traffic_ops/testing/api/v4/traffic_control_test.go b/traffic_ops/testing/api/v4/traffic_control_test.go
index a750916..7f05f8a 100644
--- a/traffic_ops/testing/api/v4/traffic_control_test.go
+++ b/traffic_ops/testing/api/v4/traffic_control_test.go
@@ -28,7 +28,7 @@ type TrafficControl struct {
 	Capabilities                                      []tc.Capability                         `json:"capability"`
 	Coordinates                                       []tc.Coordinate                         `json:"coordinates"`
 	DeliveryServicesRegexes                           []tc.DeliveryServiceRegexesTest         `json:"deliveryServicesRegexes"`
-	DeliveryServiceRequests                           []tc.DeliveryServiceRequest             `json:"deliveryServiceRequests"`
+	DeliveryServiceRequests                           []tc.DeliveryServiceRequestV40          `json:"deliveryServiceRequests"`
 	DeliveryServiceRequestComments                    []tc.DeliveryServiceRequestComment      `json:"deliveryServiceRequestComments"`
 	DeliveryServices                                  []tc.DeliveryServiceV4                  `json:"deliveryservices"`
 	DeliveryServicesRequiredCapabilities              []tc.DeliveryServicesRequiredCapability `json:"deliveryservicesRequiredCapabilities"`
diff --git a/traffic_ops/traffic_ops_golang/api/api.go b/traffic_ops/traffic_ops_golang/api/api.go
index 0d06e97..15c1ae4 100644
--- a/traffic_ops/traffic_ops_golang/api/api.go
+++ b/traffic_ops/traffic_ops_golang/api/api.go
@@ -51,6 +51,24 @@ import (
 	"github.com/lib/pq"
 )
 
+type errorConstant string
+
+func (e errorConstant) Error() string {
+	return string(e)
+}
+
+// NilRequestError is returned by APIInfo methods when the request internally
+// referred to by the APIInfo cannot be found.
+const NilRequestError = errorConstant("method called on APIInfo with nil request")
+
+// NilTransactionError is returned by APIInfo methods when the transaction
+// internally referred to by the APIInfo cannot be found.
+const NilTransactionError = errorConstant("method called on APIInfo with nil transaction")
+
+// ResourceModifiedError is a user-safe error that indicates a precondition
+// failure.
+const ResourceModifiedError = errorConstant("resource was modified")
+
 // Common context.Context value keys.
 const (
 	DBContextKey      = "db"
@@ -147,6 +165,19 @@ func WriteRespVals(w http.ResponseWriter, r *http.Request, v interface{}, vals m
 	w.Write(append(respBts, '\n'))
 }
 
+// WriteIMSHitResp writes a response to 'w' for an IMS request "hit", using the
+// passed time as the Last-Modified date.
+func WriteIMSHitResp(w http.ResponseWriter, r *http.Request, t time.Time) {
+	if respWritten(r) {
+		log.Errorf("WriteIMSHitResp called after a write already occurred! Not double-writing! Path %s", r.URL.Path)
+		return
+	}
+	setRespWritten(r)
+
+	w.Header().Add(rfc.LastModified, t.Format(rfc.LastModifiedFormat))
+	w.WriteHeader(http.StatusNotModified)
+}
+
 // HandleErr handles an API error, rolling back the transaction, writing the given statusCode and userErr to the user, and logging the sysErr. If userErr is nil, the text of the HTTP statusCode is written.
 //
 // The tx may be nil, if there is no transaction. Passing a nil tx is strongly discouraged if a transaction exists, because it will result in copy-paste errors for the common APIInfo use case.
@@ -458,6 +489,7 @@ type APIInfo struct {
 	Version   *Version
 	Tx        *sqlx.Tx
 	Config    *config.Config
+	request   *http.Request
 }
 
 // NewInfo get and returns the context info needed by handlers. It also returns any user error, any system error, and the status code which should be returned to the client if an error occurred.
@@ -525,9 +557,89 @@ func NewInfo(r *http.Request, requiredParams []string, intParamNames []string) (
 		IntParams: intParams,
 		User:      user,
 		Tx:        tx,
+		request:   r,
 	}, nil, nil, http.StatusOK
 }
 
+const createChangeLogQuery = `
+INSERT INTO log (
+	level,
+	message,
+	tm_user
+) VALUES (
+	$1,
+	$2,
+	$3
+)
+`
+
+// CreateChangeLog creates a new changelog message at the APICHANGE level for
+// the current user.
+func (inf APIInfo) CreateChangeLog(msg string) {
+	_, err := inf.Tx.Tx.Exec(createChangeLogQuery, ApiChange, msg, inf.User.ID)
+	if err != nil {
+		log.Errorf("Inserting chage log level '%s' message '%s' for user '%s': %v", ApiChange, msg, inf.User.UserName, err)
+	}
+}
+
+// UseIMS returns whether or not If-Modified-Since constraints should be used to
+// service the given request.
+func (inf APIInfo) UseIMS() bool {
+	if inf.request == nil || inf.Config == nil {
+		return false
+	}
+	return inf.Config.UseIMS && inf.request.Header.Get(rfc.IfModifiedSince) != ""
+}
+
+// CheckPrecondition checks a request's "preconditions" - its If-Match and
+// If-Unmodified-Since headers versus the last updated time of the requested
+// object(s), and returns (in order), an HTTP response code appropriate for the
+// precondition check results, a user-safe error that should be returned to
+// clients, and a server-side error that should be logged.
+// Callers must pass in a query that will return one row containing one column
+// that is the representative date/time of the last update of the requested
+// object(s), and optionally any values for placeholder arguments in the query.
+func (inf APIInfo) CheckPrecondition(query string, args ...interface{}) (int, error, error) {
+	if inf.request == nil {
+		return http.StatusInternalServerError, nil, NilRequestError
+	}
+
+	ius := inf.request.Header.Get(rfc.IfUnmodifiedSince)
+	etag := inf.request.Header.Get(rfc.IfMatch)
+	if ius == "" && etag == "" {
+		return http.StatusOK, nil, nil
+	}
+
+	if inf.Tx == nil || inf.Tx.Tx == nil {
+		return http.StatusInternalServerError, nil, NilTransactionError
+	}
+
+	var lastUpdated time.Time
+	if err := inf.Tx.Tx.QueryRow(query, args...).Scan(&lastUpdated); err != nil {
+		return http.StatusInternalServerError, nil, fmt.Errorf("scanning for lastUpdated: %v", err)
+	}
+
+	if etag != "" {
+		if et, ok := rfc.ParseETags(strings.Split(etag, ",")); ok {
+			if lastUpdated.After(et) {
+				return http.StatusPreconditionFailed, ResourceModifiedError, nil
+			}
+		}
+	}
+
+	if ius == "" {
+		return http.StatusOK, nil, nil
+	}
+
+	if tm, ok := rfc.ParseHTTPDate(ius); ok {
+		if lastUpdated.After(tm) {
+			return http.StatusPreconditionFailed, ResourceModifiedError, nil
+		}
+	}
+
+	return http.StatusOK, nil, nil
+}
+
 // Close implements the io.Closer interface. It should be called in a defer immediately after NewInfo().
 //
 // Close will commit the transaction, if it hasn't been rolled back.
@@ -1025,7 +1137,7 @@ func CheckIfUnModified(h http.Header, tx *sqlx.Tx, ID int, tableName string) (er
 		return nil, errors.New("error getting last updated: " + err.Error()), http.StatusInternalServerError
 	}
 	if !IsUnmodified(h, *existingLastUpdated) {
-		return errors.New("resource was modified"), nil, http.StatusPreconditionFailed
+		return ResourceModifiedError, nil, http.StatusPreconditionFailed
 	}
 	return nil, nil, http.StatusOK
 }
diff --git a/traffic_ops/traffic_ops_golang/api/generic_crud.go b/traffic_ops/traffic_ops_golang/api/generic_crud.go
index 4827e03..2ad292d 100644
--- a/traffic_ops/traffic_ops_golang/api/generic_crud.go
+++ b/traffic_ops/traffic_ops_golang/api/generic_crud.go
@@ -234,7 +234,7 @@ func GenericUpdate(h http.Header, val GenericUpdater) (error, error, int) {
 		return nil, err, http.StatusInternalServerError
 	}
 	if !IsUnmodified(h, *existingLastUpdated) {
-		return errors.New("resource was modified"), nil, http.StatusPreconditionFailed
+		return ResourceModifiedError, nil, http.StatusPreconditionFailed
 	}
 
 	rows, err := val.APIInfo().Tx.NamedQuery(val.UpdateQuery(), val)
diff --git a/traffic_ops/traffic_ops_golang/api/shared_handlers.go b/traffic_ops/traffic_ops_golang/api/shared_handlers.go
index 34cfa4e..f56b6a0 100644
--- a/traffic_ops/traffic_ops_golang/api/shared_handlers.go
+++ b/traffic_ops/traffic_ops_golang/api/shared_handlers.go
@@ -179,8 +179,7 @@ func ReadHandler(reader Reader) http.HandlerFunc {
 			return
 		}
 		if maxTime != nil && SetLastModifiedHeader(r, useIMS) {
-			// RFC1123
-			date := maxTime.Format("Mon, 02 Jan 2006 15:04:05 MST")
+			date := maxTime.Format(rfc.LastModifiedFormat)
 			w.Header().Add(rfc.LastModified, date)
 		}
 		w.WriteHeader(errCode)
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index 05f9db0..6999fea 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -438,6 +438,28 @@ func GetServerCapabilitiesFromName(name string, tx *sql.Tx) ([]string, error) {
 	return caps, nil
 }
 
+const dsrExistsQuery = `
+SELECT EXISTS(
+	SELECT id
+	FROM deliveryservice_request
+	WHERE status <> 'complete' AND
+		status <> 'rejected' AND
+		deliveryservice ->> 'xmlId' = $1
+)
+`
+
+// DSRExistsWithXMLID returns whether or not an **open** Delivery Service
+// Request with the given xmlid exists, and any error that occurred.
+func DSRExistsWithXMLID(xmlid string, tx *sql.Tx) (bool, error) {
+	if tx == nil {
+		return false, errors.New("checking for DSR with nil transaction")
+	}
+
+	var exists bool
+	err := tx.QueryRow(dsrExistsQuery, xmlid).Scan(&exists)
+	return exists, err
+}
+
 // ScanCachegroupsServerCapabilities, given rows of (server ID, CDN ID, cachegroup name, server capabilities),
 // returns a map of cachegroup names to server IDs, a map of server IDs to a map of their capabilities,
 // a map of server IDs to CDN IDs, and an error (if one occurs).
@@ -1105,7 +1127,7 @@ GROUP BY t.name, ds.topology
 func CheckOriginServerInDSCG(tx *sql.Tx, dsID int, dsTopology string) (error, error, int) {
 	// get servers and respective cachegroup name that have ORG type in a delivery service
 	q := `
-		SELECT s.host_name, c.name 
+		SELECT s.host_name, c.name
 		FROM server s
 			INNER JOIN deliveryservice_server ds ON ds.server = s.id
 			INNER JOIN type t ON t.id = s.type
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/assign.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/assign.go
new file mode 100644
index 0000000..631c21e
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/assign.go
@@ -0,0 +1,216 @@
+package request
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/routing/middleware"
+)
+
+// GetAssignment is the handler for GET requests to
+// /deliveryservice_requests/{{ID}}/assign.
+func GetAssignment(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	// Middleware should've already handled this, so idk why this is a pointer at all tbh
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	// This should never happen because a route doesn't exist for it
+	if version.Major < 4 {
+		w.Header().Set("Allow", http.MethodPut)
+		w.WriteHeader(http.StatusMethodNotAllowed)
+		api.WriteRespAlert(w, r, tc.ErrorLevel, http.StatusText(http.StatusMethodNotAllowed))
+		return
+	}
+
+	var dsr tc.DeliveryServiceRequestV40
+	if err := inf.Tx.QueryRowx(selectQuery+"WHERE r.id=$1", inf.IntParams["id"]).StructScan(&dsr); err != nil {
+		if err == sql.ErrNoRows {
+			errCode = http.StatusNotFound
+			userErr = fmt.Errorf("no such Delivery Service Request: %d", inf.IntParams["id"])
+			sysErr = nil
+		} else {
+			errCode = http.StatusInternalServerError
+			userErr = nil
+			sysErr = fmt.Errorf("looking for DSR: %v", err)
+		}
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+
+	authorized, err := isTenantAuthorized(dsr, inf)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	if !authorized {
+		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
+	}
+
+	api.WriteResp(w, r, dsr.Assignee)
+}
+
+type assignmentRequest struct {
+	AssigneeID *int    `json:"assigneeId"`
+	Assignee   *string `json:"assignee"`
+}
+
+func (*assignmentRequest) Validate(*sql.Tx) error {
+	return nil
+}
+
+const assignDSRQuery = `
+UPDATE deliveryservice_request
+SET assignee_id = $1
+WHERE id = $2
+RETURNING last_updated
+`
+
+func getAssignee(r *assignmentRequest, xmlID string, tx *sql.Tx) (string, int, error, error) {
+	if r == nil || tx == nil {
+		return "", http.StatusInternalServerError, nil, errors.New("nil transaction or assignment request")
+	}
+
+	var message string
+	if r.AssigneeID != nil {
+		r.Assignee = new(string)
+		if err := tx.QueryRow(`SELECT username FROM tm_user WHERE id = $1`, *r.AssigneeID).Scan(r.Assignee); err == sql.ErrNoRows {
+			userErr := fmt.Errorf("no such user #%d", *r.AssigneeID)
+			return "", http.StatusBadRequest, userErr, nil
+		} else if err != nil {
+			sysErr := fmt.Errorf("getting username for assignee ID (#%d): %v", *r.AssigneeID, err)
+			return "", http.StatusInternalServerError, nil, sysErr
+		}
+		message = fmt.Sprintf("Changed assignee of '%s' Delivery Service Request to '%s'", xmlID, *r.Assignee)
+	} else if r.Assignee != nil {
+		r.AssigneeID = new(int)
+		if err := tx.QueryRow(`SELECT id FROM tm_user WHERE username=$1`, *r.Assignee).Scan(r.AssigneeID); err == sql.ErrNoRows {
+			userErr := fmt.Errorf("no such user '%s'", *r.Assignee)
+			return "", http.StatusBadRequest, userErr, nil
+		} else if err != nil {
+			sysErr := fmt.Errorf("getting user ID for assignee (%s): %v", *r.Assignee, err)
+			return "", http.StatusInternalServerError, nil, sysErr
+		}
+		message = fmt.Sprintf("Changed assignee of '%s' Delivery Service Request to '%s'", xmlID, *r.Assignee)
+	} else {
+		message = fmt.Sprintf("Unassigned '%s' Delivery Service Request", xmlID)
+	}
+	return message, http.StatusOK, nil, nil
+}
+
+// PutAssignment is the handler for PUT requsets to
+// /deliveryservice_requests/{{ID}}/assign.
+func PutAssignment(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	var req assignmentRequest
+	if err := api.Parse(r.Body, tx, &req); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+
+	// Middleware should've already handled this, so idk why this is a pointer at all tbh
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	// Don't accept "assignee" in lieu of "assigneeId" in API version < 4.0
+	if version.Major < 4 {
+		req.Assignee = nil
+	}
+
+	var dsr tc.DeliveryServiceRequestV40
+	if err := inf.Tx.QueryRowx(selectQuery+"WHERE r.id=$1", inf.IntParams["id"]).StructScan(&dsr); err != nil {
+		if err == sql.ErrNoRows {
+			errCode = http.StatusNotFound
+			userErr = fmt.Errorf("no such Delivery Service Request: %d", inf.IntParams["id"])
+			sysErr = nil
+		} else {
+			errCode = http.StatusInternalServerError
+			userErr = nil
+			sysErr = fmt.Errorf("looking for DSR: %v", err)
+		}
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	dsr.SetXMLID()
+
+	authorized, err := isTenantAuthorized(dsr, inf)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	if !authorized {
+		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
+	}
+
+	message, errCode, userErr, sysErr := getAssignee(&req, dsr.XMLID, tx)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	if err := tx.QueryRow(assignDSRQuery, req.AssigneeID, *dsr.ID).Scan(&dsr.LastUpdated); err != nil {
+		userErr, sysErr, errCode = api.ParseDBError(err)
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	dsr.Assignee = req.Assignee
+	dsr.AssigneeID = req.AssigneeID
+
+	var resp interface{}
+	if inf.Version.Major >= 4 {
+		resp = dsr
+	} else {
+		resp = dsr.Downgrade()
+	}
+
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, message, resp)
+	// This matches the CRUDer changelog format. Note, though, that it
+	// references the DSR's ID three times and names the affected table
+	// twice. Lotta redundancy - so might be worth changing?
+	message = fmt.Sprintf("Delivery Service Request: %d, ID: %d, ACTION: %s deliveryservice_request, keys: {id:%d }", *dsr.ID, *dsr.ID, message, *dsr.ID)
+	inf.CreateChangeLog(message)
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go
index 56d44bf..d6aab44 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go
@@ -20,465 +20,743 @@ package request
  */
 
 import (
+	"database/sql"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"net/http"
-	"strconv"
+	"strings"
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
+
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/routing/middleware"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
 
 	"github.com/jmoiron/sqlx"
+	"github.com/lib/pq"
 )
 
-// TODeliveryServiceRequest is the type alias to define functions on
-type TODeliveryServiceRequest struct {
-	api.APIInfoImpl `json:"-"`
-	tc.DeliveryServiceRequestNullable
-}
+const selectQuery = `
+SELECT
+	a.username AS author,
+	e.username AS lastEditedBy,
+	s.username AS assignee,
+	r.assignee_id,
+	r.author_id,
+	r.change_type,
+	r.created_at,
+	r.id,
+	r.last_edited_by_id,
+	r.last_updated,
+	r.deliveryservice,
+	r.status
+FROM deliveryservice_request r
+JOIN tm_user a ON r.author_id = a.id
+LEFT OUTER JOIN tm_user s ON r.assignee_id = s.id
+LEFT OUTER JOIN tm_user e ON r.last_edited_by_id = e.id
+`
 
-func (v *TODeliveryServiceRequest) GetLastUpdated() (*time.Time, bool, error) {
-	return api.GetLastUpdated(v.APIInfo().Tx, *v.ID, "deliveryservice_request")
-}
+const insertQuery = `
+INSERT INTO deliveryservice_request (
+	assignee_id,
+	author_id,
+	change_type,
+	last_edited_by_id,
+	deliveryservice,
+	status
+) VALUES (
+	$1,
+	$2,
+	$3,
+	$2,
+	$4,
+	$5
+)
+RETURNING
+	id,
+	last_updated,
+	created_at
+`
 
-func (v *TODeliveryServiceRequest) SetLastUpdated(t tc.TimeNoMod) { v.LastUpdated = &t }
-func (v *TODeliveryServiceRequest) InsertQuery() string           { return insertRequestQuery() }
-func (v *TODeliveryServiceRequest) UpdateQuery() string           { return updateRequestQuery() }
-func (v *TODeliveryServiceRequest) DeleteQuery() string {
-	return `DELETE FROM deliveryservice_request WHERE id = :id`
-}
+const updateQuery = `
+UPDATE deliveryservice_request
+SET
+	assignee_id = $1,
+	change_type = $2,
+	last_edited_by_id = $3,
+	deliveryservice = $4,
+	status = $5
+WHERE id = $6
+RETURNING
+	last_updated,
+	created_at
+`
 
-func (req TODeliveryServiceRequest) GetKeyFieldsInfo() []api.KeyFieldInfo {
-	return []api.KeyFieldInfo{{"id", api.GetIntKey}}
-}
+const deleteQuery = `
+DELETE
+FROM deliveryservice_request
+WHERE id=$1
+`
 
-func (req TODeliveryServiceRequest) GetKeys() (map[string]interface{}, bool) {
-	if req.ID == nil {
-		return map[string]interface{}{"id": 0}, false
-	}
-	return map[string]interface{}{"id": *req.ID}, true
-}
+//TODO: figure out how to modify 'AddTenancyCheck' so this isn't necessary
+const customTenancyCheck = `(
+	CAST(r.deliveryservice->>'tenantId' AS BIGINT) = ANY(CAST(:accessibleTenants AS BIGINT[]))
+)`
 
-func (req *TODeliveryServiceRequest) SetKeys(keys map[string]interface{}) {
-	i, _ := keys["id"].(int) //this utilizes the non panicking type assertion, if the thrown away ok variable is false i will be the zero of the type, 0 here.
-	req.ID = &i
+func selectMaxLastUpdatedQuery(where string) string {
+	return `SELECT max(t) FROM (
+		SELECT max(r.last_updated) as t FROM deliveryservice_request r
+	JOIN tm_user a ON r.author_id = a.id
+	LEFT OUTER JOIN tm_user s ON r.assignee_id = s.id
+	LEFT OUTER JOIN tm_user e ON r.last_edited_by_id = e.id ` + where +
+		` UNION ALL
+	SELECT MAX(last_updated) AS t FROM last_deleted l WHERE l.table_name='deliveryservice_request') AS res`
 }
 
-// GetAuditName is part of the tc.Identifier interface
-func (req TODeliveryServiceRequest) GetAuditName() string {
-	return req.getXMLID()
-}
+// Get is the GET handler for /deliveryservice_requests.
+func Get(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
 
-// GetType is part of the tc.Identifier interface
-func (req TODeliveryServiceRequest) GetType() string {
-	return "deliveryservice_request"
-}
+	// Middleware should've already handled this, so idk why this is a pointer at all tbh
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
 
-// Read implements the api.Reader interface
-func (req *TODeliveryServiceRequest) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
-	var maxTime time.Time
-	var runSecond bool
-	deliveryServiceRequests := []interface{}{}
 	queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
-		"assignee":   dbhelpers.WhereColumnInfo{Column: "s.username"},
-		"assigneeId": dbhelpers.WhereColumnInfo{Column: "r.assignee_id", Checker: api.IsInt},
-		"author":     dbhelpers.WhereColumnInfo{Column: "a.username"},
-		"authorId":   dbhelpers.WhereColumnInfo{Column: "r.author_id", Checker: api.IsInt},
-		"changeType": dbhelpers.WhereColumnInfo{Column: "r.change_type"},
-		"createdAt":  dbhelpers.WhereColumnInfo{Column: "r.created_at"},
-		"id":         dbhelpers.WhereColumnInfo{Column: "r.id", Checker: api.IsInt},
-		"status":     dbhelpers.WhereColumnInfo{Column: "r.status"},
-		"xmlId":      dbhelpers.WhereColumnInfo{Column: "r.deliveryservice->>'xmlId'"},
-	}
-
-	p := req.APIInfo().Params
-	if _, ok := req.APIInfo().Params["orderby"]; !ok {
-		// if orderby not provided, default to orderby xmlId.  Making a copy of parameters to not modify input arg
-		p = make(map[string]string, len(req.APIInfo().Params))
-		for k, v := range req.APIInfo().Params {
-			p[k] = v
-		}
-		p["orderby"] = "xmlId"
+		"assignee":   {Column: "s.username"},
+		"assigneeId": {Column: "r.assignee_id", Checker: api.IsInt},
+		"author":     {Column: "a.username"},
+		"authorId":   {Column: "r.author_id", Checker: api.IsInt},
+		"changeType": {Column: "r.change_type"},
+		"createdAt":  {Column: "r.created_at"},
+		"id":         {Column: "r.id", Checker: api.IsInt},
+		"status":     {Column: "r.status"},
+	}
+	if _, ok := inf.Params["orderby"]; !ok {
+		inf.Params["orderby"] = "xmlId"
 	}
 
-	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(p, queryParamsToQueryCols)
+	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, queryParamsToQueryCols)
 	if len(errs) > 0 {
-		return nil, util.JoinErrs(errs), nil, http.StatusBadRequest, nil
+		api.HandleErr(w, r, tx, http.StatusBadRequest, util.JoinErrs(errs), nil)
+		return
 	}
-	if useIMS {
-		runSecond, maxTime = ims.TryIfModifiedSinceQuery(req.APIInfo().Tx, h, queryValues, selectMaxLastUpdatedQuery(where))
+
+	// TODO: add this functionality to the query builder in dbhelpers
+	if xmlID, ok := inf.Params["xmlId"]; ok {
+		queryValues["xmlId"] = xmlID
+		if where != "" {
+			where += " AND "
+		} else {
+			where = "WHERE "
+		}
+		where += "(r.deliveryservice->>'xmlId' = :xmlId)"
+	}
+
+	var maxTime *time.Time
+	if inf.UseIMS() {
+		maxTime = new(time.Time)
+		var runSecond bool
+		runSecond, *maxTime = ims.TryIfModifiedSinceQuery(inf.Tx, r.Header, queryValues, selectMaxLastUpdatedQuery(where))
 		if !runSecond {
 			log.Debugln("IMS HIT")
-			return deliveryServiceRequests, nil, nil, http.StatusNotModified, &maxTime
+			api.WriteIMSHitResp(w, r, *maxTime)
+			return
 		}
 		log.Debugln("IMS MISS")
 	} else {
 		log.Debugln("Non IMS request")
 	}
-	tenantIDs, err := tenant.GetUserTenantIDListTx(req.APIInfo().Tx.Tx, req.APIInfo().User.TenantID)
+
+	tenantIDs, err := tenant.GetUserTenantIDListTx(tx, inf.User.TenantID)
 	if err != nil {
-		return nil, nil, errors.New("dsr getting tenant list: " + err.Error()), http.StatusInternalServerError, nil
+		sysErr = fmt.Errorf("dsr getting tenant list: %v", err)
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr)
+		return
+	}
+
+	if where == "" {
+		where = "WHERE "
+	} else {
+		where += " AND "
 	}
-	where, queryValues = dbhelpers.AddTenancyCheck(where, queryValues, "CAST(r.deliveryservice->>'tenantId' AS bigint)", tenantIDs)
+	where += customTenancyCheck
+	queryValues["accessibleTenants"] = pq.Array(tenantIDs)
 
-	query := selectDeliveryServiceRequestsQuery() + where + orderBy + pagination
+	query := selectQuery + where + orderBy + pagination
 	log.Debugln("Query is ", query)
 
-	rows, err := req.APIInfo().Tx.NamedQuery(query, queryValues)
+	rows, err := inf.Tx.NamedQuery(query, queryValues)
 	if err != nil {
-		return nil, nil, errors.New("dsr querying: " + err.Error()), http.StatusInternalServerError, &maxTime
+		sysErr = fmt.Errorf("dsr querying: %v", err)
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr)
+		return
 	}
 	defer rows.Close()
 
+	dsrs := []tc.DeliveryServiceRequestV40{}
 	for rows.Next() {
-		var s TODeliveryServiceRequest
-		if err = rows.StructScan(&s); err != nil {
-			return nil, nil, errors.New("dsr scanning: " + err.Error()), http.StatusInternalServerError, &maxTime
+		var dsr tc.DeliveryServiceRequestV40
+		if err = rows.StructScan(&dsr); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("dsr scanning: %v", err))
+			return
 		}
-		deliveryServiceRequests = append(deliveryServiceRequests, s)
+		dsrs = append(dsrs, dsr)
 	}
 
-	return deliveryServiceRequests, nil, nil, http.StatusOK, &maxTime
+	if maxTime != nil {
+		w.Header().Set(rfc.LastModified, maxTime.Format(rfc.LastModifiedFormat))
+	}
+
+	if version.Major >= 4 {
+		api.WriteResp(w, r, dsrs)
+	}
+
+	downgraded := make([]tc.DeliveryServiceRequestNullable, 0, len(dsrs))
+	for _, dsr := range dsrs {
+		downgraded = append(downgraded, dsr.Downgrade())
+	}
+
+	api.WriteResp(w, r, downgraded)
 }
 
-func selectMaxLastUpdatedQuery(where string) string {
-	return `SELECT max(t) from (
-		SELECT max(r.last_updated) as t FROM deliveryservice_request r
-	JOIN tm_user a ON r.author_id = a.id
-	LEFT OUTER JOIN tm_user s ON r.assignee_id = s.id
-	LEFT OUTER JOIN tm_user e ON r.last_edited_by_id = e.id ` + where +
-		` UNION ALL
-	select max(last_updated) as t from last_deleted l where l.table_name='deliveryservice_request') as res`
+// isTenantAuthorized ensures the user is authorized on the DSR's
+// DeliveryService's Tenant, as appropriate to the change type.
+func isTenantAuthorized(dsr tc.DeliveryServiceRequestV40, inf *api.APIInfo) (bool, error) {
+	if dsr.DeliveryService != nil && (dsr.ChangeType == tc.DSRChangeTypeUpdate || dsr.ChangeType == tc.DSRChangeTypeCreate) {
+		if dsr.DeliveryService.TenantID == nil {
+			return false, errors.New("deliveryService.tenantID is nil")
+		}
+		ok, err := tenant.IsResourceAuthorizedToUserTx(*dsr.DeliveryService.TenantID, inf.User, inf.Tx.Tx)
+		if !ok || err != nil {
+			return ok, err
+		}
+	}
+
+	return true, nil
 }
 
-func selectDeliveryServiceRequestsQuery() string {
-
-	query := `SELECT
-a.username AS author,
-e.username AS lastEditedBy,
-s.username AS assignee,
-r.assignee_id,
-r.author_id,
-r.change_type,
-r.created_at,
-r.id,
-r.last_edited_by_id,
-r.last_updated,
-r.deliveryservice,
-r.status,
-r.deliveryservice->>'xmlId' as xml_id
+// Warning: this assumes inf isn't nil, and neither is dsr, inf.Tx or inf.User or inf.Tx.Tx.
+func insert(dsr *tc.DeliveryServiceRequestV40, inf *api.APIInfo) (int, error, error) {
+	dsr.Author = inf.User.UserName
+	dsr.LastEditedBy = inf.User.UserName
 
-FROM deliveryservice_request r
-JOIN tm_user a ON r.author_id = a.id
-LEFT OUTER JOIN tm_user s ON r.assignee_id = s.id
-LEFT OUTER JOIN tm_user e ON r.last_edited_by_id = e.id
-`
-	return query
+	dsr.ID = new(int)
+	if err := inf.Tx.Tx.QueryRow(insertQuery, dsr.AssigneeID, inf.User.ID, dsr.ChangeType, dsr.DeliveryService, dsr.Status).Scan(dsr.ID, &dsr.LastUpdated, &dsr.CreatedAt); err != nil {
+		userErr, sysErr, errCode := api.ParseDBError(err)
+		return errCode, userErr, sysErr
+	}
+	return http.StatusOK, nil, nil
 }
 
-// IsTenantAuthorized implements the Tenantable interface to ensure the user is authorized on the deliveryservice tenant
-func (req TODeliveryServiceRequest) IsTenantAuthorized(user *auth.CurrentUser) (bool, error) {
+// dsrManipulationResult encodes the result of manipulating a DSR.
+type dsrManipulationResult struct {
+	// Action is the action performed to manipulate the DSR.
+	Action string
+	// Assignee is a pointer to the name of the user assigned to a DSR - or nil
+	// if there isn't one.
+	Assignee *string
+	// ChangeType is the DSR's change type.
+	ChangeType tc.DSRChangeType
+	// Successful is whether or not the manipulation encountered no errors.
+	Successful bool
+	// XMLID is the XMLID of the Delivery Service affected by the DSR.
+	XMLID string
+}
 
-	ds := req.DeliveryService
-	if ds == nil {
-		// No deliveryservice applied yet -- wide open
-		return true, nil
+// String constructs a changelog message for the result.
+// Unsuccessful results do not have a changelog message.
+func (d dsrManipulationResult) String() string {
+	if !d.Successful {
+		return ""
 	}
-	if ds.TenantID == nil {
-		log.Debugf("tenantID is nil")
-		return false, errors.New("tenantID is nil")
+
+	var builder strings.Builder
+	builder.WriteString(d.Action)
+	builder.Write([]byte(" Delivery Service Request of type "))
+	builder.WriteString(d.ChangeType.String())
+	builder.Write([]byte(" for Delivery Service '"))
+	builder.WriteString(d.XMLID)
+	builder.WriteRune('\'')
+
+	if d.Assignee != nil {
+		builder.Write([]byte(" (assigned to user "))
+		builder.WriteString(*d.Assignee)
+		builder.WriteRune(')')
 	}
-	return tenant.IsResourceAuthorizedToUserTx(*ds.TenantID, user, req.APIInfo().Tx.Tx)
+
+	return builder.String()
 }
 
-// Update implements the tc.Updater interface.
-//all implementations of Updater should use transactions and return the proper errorType
-//ParsePQUniqueConstraintError is used to determine if a request with conflicting values exists
-//if so, it will return an errorType of DataConflict and the type should be appended to the
-//generic error message returned
-func (req *TODeliveryServiceRequest) Update(h http.Header) (error, error, int) {
-	if req.ID == nil {
-		return errors.New("missing id"), nil, http.StatusBadRequest
+func createV4(w http.ResponseWriter, r *http.Request, inf *api.APIInfo) (result dsrManipulationResult) {
+	tx := inf.Tx.Tx
+	var dsr tc.DeliveryServiceRequestV40
+	if err := json.NewDecoder(r.Body).Decode(&dsr); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("decoding: %v", err), nil)
+		return
+	}
+	if userErr, sysErr := validateV4(dsr, tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, sysErr)
+		return
 	}
 
-	current := TODeliveryServiceRequest{}
-	err := req.ReqInfo.Tx.QueryRowx(selectDeliveryServiceRequestsQuery()+`WHERE r.id=$1`, *req.ID).StructScan(&current)
-	if err != nil {
-		return nil, errors.New("dsr update querying: " + err.Error()), http.StatusInternalServerError
+	if dsr.Status != tc.RequestStatusDraft && dsr.Status != tc.RequestStatusSubmitted {
+		userErr := fmt.Errorf("invalid initial request status '%s' - must be '%s' or '%s'", dsr.Status, tc.RequestStatusDraft, tc.RequestStatusSubmitted)
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+		return
 	}
 
-	// Update can only change status between draft & submitted.  All other transitions must go thru
-	// the PUT /api/<version>/deliveryservice_request/:id/status endpoint
-	if current.Status == nil || req.Status == nil {
-		return errors.New("Missing status for DeliveryServiceRequest"), nil, http.StatusBadRequest
+	ok, err := isTenantAuthorized(dsr, inf)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	if !ok {
+		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
 	}
 
-	if *current.Status != tc.RequestStatusDraft && *current.Status != tc.RequestStatusSubmitted {
-		return fmt.Errorf("Cannot change DeliveryServiceRequest in '%s' status.", string(*current.Status)), nil, http.StatusBadRequest
+	dsr.SetXMLID()
+	if ok, err = dbhelpers.DSRExistsWithXMLID(dsr.XMLID, tx); err != nil {
+		err = fmt.Errorf("checking for existence of DSR with xmlid '%s'", dsr.XMLID)
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	} else if ok {
+		userErr := fmt.Errorf("an open Delivery Service Request for XMLID '%s' already exists", dsr.XMLID)
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+		return
 	}
 
-	if *req.Status != tc.RequestStatusDraft && *req.Status != tc.RequestStatusSubmitted {
-		return fmt.Errorf("Cannot change DeliveryServiceRequest status from '%s' to '%s'", string(*current.Status), string(*req.Status)), nil, http.StatusBadRequest
+	errCode, userErr, sysErr := insert(&dsr, inf)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
 	}
 
-	userID := tc.IDNoMod(req.APIInfo().User.ID)
-	req.LastEditedByID = &userID
+	w.Header().Set("Location", fmt.Sprintf("/api/%d.%d/deliveryservice_requests/%d", inf.Version.Major, inf.Version.Minor, *dsr.ID))
+	w.WriteHeader(http.StatusCreated)
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service request created", dsr)
 
-	return api.GenericUpdate(h, req)
+	result.Successful = true
+	result.Assignee = dsr.Assignee
+	result.XMLID = dsr.XMLID
+	result.ChangeType = dsr.ChangeType
+	result.Action = api.Created
+	return
 }
 
-// Creator implements the tc.Creator interface
-//all implementations of Creator should use transactions and return the proper errorType
-//ParsePQUniqueConstraintError is used to determine if a request with conflicting values exists
-//if so, it will return an errorType of DataConflict and the type should be appended to the
-//generic error message returned
-//The insert sql returns the id and lastUpdated values of the newly inserted request and have
-//to be added to the struct
-func (req *TODeliveryServiceRequest) Create() (error, error, int) {
-	// TODO move to Validate()
-	if req.Status == nil {
-		return errors.New("missing status"), nil, http.StatusBadRequest
-	}
-	if *req.Status != tc.RequestStatusDraft && *req.Status != tc.RequestStatusSubmitted {
-		return fmt.Errorf("invalid initial request status '%v'.  Must be '%v' or '%v'",
-			*req.Status, tc.RequestStatusDraft, tc.RequestStatusSubmitted), nil, http.StatusBadRequest
+func createLegacy(w http.ResponseWriter, r *http.Request, inf *api.APIInfo) (result dsrManipulationResult) {
+	tx := inf.Tx.Tx
+	var dsr tc.DeliveryServiceRequestNullable
+	if err := json.NewDecoder(r.Body).Decode(&dsr); err != nil {
+		userErr := fmt.Errorf("decoding: %v", err)
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+		return
+	}
+	if err := validateLegacy(dsr, tx); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+
+	upgraded := dsr.Upgrade()
+	authorized, err := isTenantAuthorized(upgraded, inf)
+	if err != nil {
+		sysErr := fmt.Errorf("checking tenant authorized: %v", err)
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr)
+		return
+	}
+	if !authorized {
+		userErr := errors.New("not authorized on this tenant")
+		api.HandleErr(w, r, tx, http.StatusForbidden, userErr, nil)
+		return
+	}
+
+	if *dsr.Status != tc.RequestStatusDraft && *dsr.Status != tc.RequestStatusSubmitted {
+		userErr := fmt.Errorf("invalid initial request status '%v'. Must be '%v' or '%v'", *dsr.Status, tc.RequestStatusDraft, tc.RequestStatusSubmitted)
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+		return
 	}
+
 	// first, ensure there's not an active request with this XMLID
-	ds := req.DeliveryService
+	ds := dsr.DeliveryService
 	if ds == nil {
-		return errors.New("no delivery service associated with this request"), nil, http.StatusBadRequest
+		userErr := errors.New("no delivery service associated with this request")
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+		return
 	}
+
 	if ds.XMLID == nil {
-		return errors.New("no xmlId associated with this request"), nil, http.StatusBadRequest
+		userErr := errors.New("no XMLID associated with this request")
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+		return
 	}
 	XMLID := *ds.XMLID
-	active, err := isActiveRequest(req.APIInfo().Tx, XMLID)
+	active, err := isActiveRequest(inf.Tx, XMLID)
 	if err != nil {
-		return errors.New("checking request active: " + err.Error()), nil, http.StatusInternalServerError
+		sysErr := fmt.Errorf("checking request active: %v", err)
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr)
+		return
 	}
 	if active {
-		return errors.New(`An active request exists for delivery service '` + XMLID + `'`), nil, http.StatusBadRequest
+		userErr := fmt.Errorf("an active request exists for Delivery Service '%s'", XMLID)
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+		return
 	}
 
-	userID := tc.IDNoMod(req.APIInfo().User.ID)
-	req.AuthorID = &userID
-	req.LastEditedByID = &userID
-
-	return api.GenericCreate(req)
-}
-
-func (req *TODeliveryServiceRequest) Delete() (error, error, int) {
-	if req.ID == nil {
-		return errors.New("missing id"), nil, http.StatusBadRequest
+	errCode, userErr, sysErr := insert(&upgraded, inf)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
 	}
 
-	st := tc.RequestStatusInvalid
-	if err := req.APIInfo().Tx.Tx.QueryRow(`SELECT status FROM deliveryservice_request WHERE id=$1`, *req.ID).Scan(&st); err != nil {
-		return nil, errors.New("dsr delete querying status: " + err.Error()), http.StatusBadRequest
-	}
-	if st == tc.RequestStatusComplete || st == tc.RequestStatusPending || st == tc.RequestStatusRejected {
-		return errors.New("cannot delete a deliveryservice_request with state " + string(st)), nil, http.StatusBadRequest
-	}
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service request created", upgraded.Downgrade())
 
-	return api.GenericDelete(req)
+	result.Successful = true
+	result.Assignee = dsr.Assignee
+	result.XMLID = upgraded.XMLID
+	result.ChangeType = upgraded.ChangeType
+	result.Action = api.Created
+	return
 }
 
-func (req TODeliveryServiceRequest) getXMLID() string {
-	if req.DeliveryService == nil || req.DeliveryService.XMLID == nil {
-
-		if req.ID != nil {
-			return strconv.Itoa(*req.ID)
-		}
-		return "0"
+// Post is the handler for POST requests to /deliveryservice_requests.
+func Post(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
 	}
-	return *req.DeliveryService.XMLID
-}
+	defer inf.Close()
 
-// ChangeLogMessage implements the api.ChangeLogger interface for a custom log message
-func (req TODeliveryServiceRequest) ChangeLogMessage(action string) (string, error) {
-	changeType := "unknown change type"
-	if req.ChangeType != nil {
-		changeType = *req.ChangeType
+	// Middleware should've already handled this, so idk why this is a pointer at all tbh
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
 	}
-	// use ID in case don't have access to XMLID (e.g. on DELETE)
-	message := action + ` ` + req.GetType() + ` of type '` + changeType + `' for deliveryservice '` + req.getXMLID() + `'`
-	return message, nil
-}
-
-// isActiveRequest returns true if a request using this XMLID is currently in an active state
-func isActiveRequest(tx *sqlx.Tx, xmlID string) (bool, error) {
-	qry := `SELECT EXISTS(SELECT 1 FROM deliveryservice_request WHERE deliveryservice->>'xmlId' = $1 AND status IN ('draft', 'submitted', 'pending'))`
-	active := false
-	if err := tx.QueryRow(qry, xmlID).Scan(&active); err != nil {
-		return false, err
+	if inf.User == nil {
+		sysErr = errors.New("no user in API Info")
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, sysErr)
+		return
 	}
-	return active, nil
-}
-
-func updateRequestQuery() string {
-	query := `UPDATE
-deliveryservice_request
-SET change_type=:change_type,
-last_edited_by_id=:last_edited_by_id,
-deliveryservice=:deliveryservice,
-status=:status
-WHERE id=:id RETURNING last_updated`
-	return query
-}
-
-func insertRequestQuery() string {
-	query := `INSERT INTO deliveryservice_request (
-assignee_id,
-author_id,
-change_type,
-last_edited_by_id,
-deliveryservice,
-status
-) VALUES (
-:assignee_id,
-:author_id,
-:change_type,
-:last_edited_by_id,
-:deliveryservice,
-:status
-) RETURNING id,last_updated`
-	return query
-}
-
-////////////////////////////////////////////////////////////////
-// Assignment change
 
-func GetAssignmentSingleton() api.CRUDer {
-	return &deliveryServiceRequestAssignment{}
-}
+	var result dsrManipulationResult
+	if version.Major >= 4 {
+		result = createV4(w, r, inf)
+	} else {
+		result = createLegacy(w, r, inf)
+	}
 
-type deliveryServiceRequestAssignment struct {
-	TODeliveryServiceRequest
+	if result.Successful {
+		inf.CreateChangeLog(result.String())
+	}
 }
 
-// Update assignee only
-func (req *deliveryServiceRequestAssignment) Update(h http.Header) (error, error, int) {
-	// req represents the state the deliveryservice_request is to transition to
-	// we want to limit what changes here -- only assignee can change
-	if req.ID == nil {
-		return errors.New("missing id"), nil, http.StatusBadRequest
+// Delete is the handler for DELETE requests to /deliveryservice_requests.
+func Delete(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	// Middleware should've already handled this, so idk why this is a pointer at all tbh
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	if inf.User == nil {
+		sysErr = errors.New("no user in API Info")
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, sysErr)
+		return
+	}
+
+	var dsr tc.DeliveryServiceRequestV40
+	if err := inf.Tx.QueryRowx(selectQuery+"WHERE r.id=$1", inf.IntParams["id"]).StructScan(&dsr); err != nil {
+		if err == sql.ErrNoRows {
+			errCode = http.StatusNotFound
+			userErr = fmt.Errorf("no such Delivery Service Request: #%d", inf.IntParams["id"])
+			sysErr = nil
+		} else {
+			userErr, sysErr, errCode = api.ParseDBError(err)
+		}
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
 	}
+	dsr.SetXMLID()
 
-	current := TODeliveryServiceRequest{}
-	err := req.ReqInfo.Tx.QueryRowx(selectDeliveryServiceRequestsQuery()+`WHERE r.id = $1`, *req.ID).StructScan(&current)
+	authorized, err := isTenantAuthorized(dsr, inf)
 	if err != nil {
-		return nil, errors.New("dsr assignment querying existing: " + err.Error()), http.StatusInternalServerError
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	if !authorized {
+		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
 	}
 
-	// unchanged (maybe both nil)
-	if current.AssigneeID == req.AssigneeID {
-		log.Infof("dsr assignment update: assignee unchanged")
-		return nil, nil, http.StatusOK
+	result, err := tx.Exec(deleteQuery, inf.IntParams["id"])
+	if err != nil {
+		sysErr = fmt.Errorf("deleting DSR #%d: %v", inf.IntParams["id"], err)
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr)
+		return
+	}
+	if affected, err := result.RowsAffected(); err != nil {
+		sysErr = fmt.Errorf("checking affected rows: %v", err)
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr)
+		return
+	} else if affected != 1 {
+		sysErr = fmt.Errorf("incorrect number of rows affected by delete: %d", affected)
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr)
+		return
+	}
+
+	var resp interface{}
+	if inf.Version.Major >= 4 {
+		resp = dsr
+	} else {
+		resp = dsr.Downgrade()
 	}
 
-	// Only assigneeID changes -- nothing else
-	assigneeID := req.AssigneeID
-	req.DeliveryServiceRequestNullable = current.DeliveryServiceRequestNullable
-	req.AssigneeID = assigneeID
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, fmt.Sprintf("Delivery Service Request #%d deleted", inf.IntParams["id"]), resp)
 
-	userErr, sysErr, statusCode := api.CheckIfUnModified(h, req.ReqInfo.Tx, *req.ID, "deliveryservice_request")
-	if userErr != nil || sysErr != nil {
-		return userErr, sysErr, statusCode
+	res := dsrManipulationResult{
+		Successful: true,
+		XMLID:      dsr.XMLID,
+		Action:     api.Deleted,
+		Assignee:   dsr.Assignee,
+		ChangeType: dsr.ChangeType,
 	}
+	inf.CreateChangeLog(res.String())
+}
 
-	// LastEditedBy field should not change with status update
-	if _, err = req.APIInfo().Tx.Tx.Exec(`UPDATE deliveryservice_request SET assignee_id = $1 WHERE id = $2`, req.AssigneeID, *req.ID); err != nil {
-		return api.ParseDBError(err)
+func putV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo) (result dsrManipulationResult) {
+	tx := inf.Tx.Tx
+	var dsr tc.DeliveryServiceRequestV40
+	if err := json.NewDecoder(r.Body).Decode(&dsr); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("decoding: %v", err), nil)
+		return
 	}
-
-	if err = req.APIInfo().Tx.QueryRowx(selectDeliveryServiceRequestsQuery()+` WHERE r.id = $1`, *req.ID).StructScan(req); err != nil {
-		return nil, errors.New("dsr assignment querying: " + err.Error()), http.StatusInternalServerError
+	if userErr, sysErr := validateV4(dsr, tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, sysErr)
+		return
 	}
 
-	return nil, nil, http.StatusOK
-}
+	if dsr.Status != tc.RequestStatusDraft && dsr.Status != tc.RequestStatusSubmitted {
+		userErr := fmt.Errorf("cannot change DeliveryServiceRequest status to '%s'", dsr.Status)
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+		return
+	}
 
-func (req deliveryServiceRequestAssignment) Validate() error {
-	return nil
+	authorized, err := isTenantAuthorized(dsr, inf)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	if !authorized {
+		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
+	}
+
+	dsr.LastEditedBy = inf.User.UserName
+	dsr.LastEditedByID = new(int)
+	*dsr.LastEditedByID = inf.User.ID
+
+	args := []interface{}{
+		dsr.AssigneeID,
+		dsr.ChangeType,
+		inf.User.ID,
+		dsr.DeliveryService,
+		dsr.Status,
+		inf.IntParams["id"],
+	}
+	if err := tx.QueryRow(updateQuery, args...).Scan(&dsr.CreatedAt, &dsr.LastUpdated); err != nil {
+		var userErr, sysErr error
+		var errCode int
+		if err == sql.ErrNoRows {
+			userErr = fmt.Errorf("no such Delivery Service Request: #%d", inf.IntParams["id"])
+			errCode = http.StatusNotFound
+			sysErr = fmt.Errorf("running update query for Delivery Service Requests: %v", err)
+		} else {
+			userErr, sysErr, errCode = api.ParseDBError(err)
+		}
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	dsr.SetXMLID()
+
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, fmt.Sprintf("Delivery Service Request #%d updated", inf.IntParams["id"]), dsr)
+	result.Successful = true
+	result.Action = "Updated"
+	result.Assignee = dsr.Assignee
+	result.ChangeType = dsr.ChangeType
+	result.XMLID = dsr.XMLID
+	return
 }
 
-// ChangeLogMessage implements the api.ChangeLogger interface for a custom log message
-func (req deliveryServiceRequestAssignment) ChangeLogMessage(action string) (string, error) {
-	a := "NONE"
-	if req.Assignee != nil {
-		a = *req.Assignee
+func putLegacy(w http.ResponseWriter, r *http.Request, inf *api.APIInfo) (result dsrManipulationResult) {
+	tx := inf.Tx.Tx
+	var dsr tc.DeliveryServiceRequestNullable
+	if err := json.NewDecoder(r.Body).Decode(&dsr); err != nil {
+		userErr := fmt.Errorf("decoding: %v", err)
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+		return
+	}
+	if err := validateLegacy(dsr, tx); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
 	}
-	message := `Changed assignee of ‘` + req.getXMLID() + `’ ` + req.GetType() + ` to '` + a + `'`
 
-	return message, nil
-}
+	if *dsr.Status != tc.RequestStatusDraft && *dsr.Status != tc.RequestStatusSubmitted {
+		userErr := fmt.Errorf("cannot change DeliveryServiceRequest status to '%s'", dsr.Status)
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+		return
+	}
 
-////////////////////////////////////////////////////////////////
-// Status change
+	dsr.LastEditedBy = new(string)
+	*dsr.LastEditedBy = inf.User.UserName
+	dsr.LastEditedByID = new(tc.IDNoMod)
+	*dsr.LastEditedByID = tc.IDNoMod(inf.User.ID)
 
-func GetStatusSingleton() api.CRUDer {
-	return &deliveryServiceRequestStatus{}
-}
+	upgraded := dsr.Upgrade()
 
-// deliveryServiceRequestStatus implements interfaces needed to update the request status only
-type deliveryServiceRequestStatus struct {
-	TODeliveryServiceRequest
+	authorized, err := isTenantAuthorized(upgraded, inf)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	if !authorized {
+		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
+	}
+
+	args := []interface{}{
+		upgraded.AssigneeID,
+		upgraded.ChangeType,
+		inf.User.ID,
+		upgraded.DeliveryService,
+		upgraded.Status,
+		inf.IntParams["id"],
+	}
+	if err := tx.QueryRow(updateQuery, args...).Scan(&dsr.CreatedAt, &dsr.LastUpdated); err != nil {
+		var errCode int
+		var userErr, sysErr error
+		if err == sql.ErrNoRows {
+			errCode = http.StatusNotFound
+			userErr = fmt.Errorf("no such Delivery Service Request: #%d", inf.IntParams["id"])
+			sysErr = nil
+		} else {
+			userErr, sysErr, errCode = api.ParseDBError(err)
+		}
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	upgraded.SetXMLID()
+
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, fmt.Sprintf("Delivery Service Request #%d updated", inf.IntParams["id"]), dsr)
+	result.Action = api.Updated
+	result.Assignee = dsr.Assignee
+	result.ChangeType = upgraded.ChangeType
+	result.Successful = true
+	result.XMLID = upgraded.XMLID
+	return
 }
 
-func (req *deliveryServiceRequestStatus) Update(h http.Header) (error, error, int) {
-	// req represents the state the deliveryservice_request is to transition to
-	// we want to limit what changes here -- only status can change,  and only according to the established rules
-	// for status transition
-	if req.ID == nil {
-		return errors.New("missing id"), nil, http.StatusBadRequest
+// Put is the handler for PUT requests to /deliveryservice_requests.
+func Put(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
 	}
+	defer inf.Close()
 
-	current := TODeliveryServiceRequest{}
-	err := req.APIInfo().Tx.QueryRowx(selectDeliveryServiceRequestsQuery()+` WHERE r.id = $1`, *req.ID).StructScan(&current)
-	if err != nil {
-		return nil, errors.New("dsr status querying existing: " + err.Error()), http.StatusInternalServerError
+	// Middleware should've already handled this, so idk why this is a pointer at all tbh
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
 	}
-
-	if err = current.Status.ValidTransition(*req.Status); err != nil {
-		return err, nil, http.StatusBadRequest // TODO verify err is secure to send to user
+	if inf.User == nil {
+		sysErr = errors.New("no user in API Info")
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr)
+		return
 	}
 
-	// keep everything else the same -- only update status
-	st := req.Status
-	req.DeliveryServiceRequestNullable = current.DeliveryServiceRequestNullable
-	req.Status = st
+	id := inf.IntParams["id"]
 
-	// LastEditedBy field should not change with status update
-	existingLastUpdated := current.LastUpdated
-	if !api.IsUnmodified(h, existingLastUpdated.Time) {
-		return errors.New("resource was modified"), nil, http.StatusPreconditionFailed
+	errCode, userErr, sysErr = inf.CheckPrecondition(selectMaxLastUpdatedQuery("WHERE r.id = $1"), id)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+
+	var current tc.DeliveryServiceRequestV40
+	if err := inf.Tx.QueryRowx(selectQuery+"WHERE r.id=$1", id).StructScan(&current); err != nil {
+		if err == sql.ErrNoRows {
+			errCode = http.StatusNotFound
+			userErr = fmt.Errorf("no such Delivery Service Request: #%d", inf.IntParams["id"])
+			sysErr = nil
+		} else {
+			userErr, sysErr, errCode = api.ParseDBError(err)
+		}
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
 	}
 
-	if _, err = req.APIInfo().Tx.Tx.Exec(`UPDATE deliveryservice_request SET status = $1 WHERE id = $2`, *req.Status, *req.ID); err != nil {
-		return api.ParseDBError(err)
+	authorized, err := isTenantAuthorized(current, inf)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	if !authorized {
+		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
 	}
 
-	if err = req.APIInfo().Tx.QueryRowx(selectDeliveryServiceRequestsQuery()+` WHERE r.id = $1`, *req.ID).StructScan(req); err != nil {
-		return nil, errors.New("dsr status update querying: " + err.Error()), http.StatusInternalServerError
+	if !current.IsOpen() {
+		userErr = fmt.Errorf("cannot change DeliveryServiceRequest in '%s' status", current.Status)
+		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+		return
 	}
 
-	return nil, nil, http.StatusOK
-}
+	var result dsrManipulationResult
+	if inf.Version.Major >= 4 {
+		result = putV40(w, r, inf)
+	} else {
+		result = putLegacy(w, r, inf)
+	}
 
-// Validate is not needed when only Status is updated
-func (req deliveryServiceRequestStatus) Validate() error {
-	return nil
+	if result.Successful {
+		inf.CreateChangeLog(result.String())
+	}
 }
 
-// ChangeLogMessage implements the api.ChangeLogger interface for a custom log message
-func (req deliveryServiceRequestStatus) ChangeLogMessage(action string) (string, error) {
-	message := `Changed status of ‘` + req.getXMLID() + `’ ` + req.GetType() + ` to '` + string(*req.Status) + `'`
-	return message, nil
+// isActiveRequest returns true if a request using this XMLID is currently in an active state.
+func isActiveRequest(tx *sqlx.Tx, xmlID string) (bool, error) {
+	qry := `SELECT EXISTS(SELECT 1 FROM deliveryservice_request WHERE deliveryservice->>'xmlId' = $1 AND status IN ('draft', 'submitted', 'pending'))`
+	active := false
+	if err := tx.QueryRow(qry, xmlID).Scan(&active); err != nil {
+		return false, err
+	}
+	return active, nil
 }
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/requests_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/requests_test.go
index d2fbeeb..7e403fe 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/request/requests_test.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/requests_test.go
@@ -22,125 +22,151 @@ package request
 import (
 	"testing"
 
-	tc "github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 	"github.com/jmoiron/sqlx"
-	sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1"
+	"gopkg.in/DATA-DOG/go-sqlmock.v1"
 )
 
-func TestInterfaces(t *testing.T) {
-	var i interface{}
-	i = &TODeliveryServiceRequest{}
+func TestGetAssignee(t *testing.T) {
+	req := assignmentRequest{
+		AssigneeID: nil,
+		Assignee:   nil,
+	}
 
-	if _, ok := i.(api.Creator); !ok {
-		t.Errorf("DeliveryServiceRequest must be Creator")
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("opening mock database: %v", err)
 	}
-	if _, ok := i.(api.Reader); !ok {
-		t.Errorf("DeliveryServiceRequest must be Reader")
+	defer mockDB.Close()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	// check simple case, no Assignee or ID means no change
+	mock.ExpectBegin()
+	_, _, userErr, sysErr := getAssignee(&req, "test", db.MustBegin().Tx)
+	if userErr != nil {
+		t.Errorf("unexpected user error: %v", userErr)
 	}
-	if _, ok := i.(api.Updater); !ok {
-		t.Errorf("DeliveryServiceRequest must be Updater")
+	if sysErr != nil {
+		t.Errorf("unexpected system error: %v", sysErr)
 	}
-	if _, ok := i.(api.Deleter); !ok {
-		t.Errorf("DeliveryServiceRequest must be Deleter")
+	if req.AssigneeID != nil {
+		t.Errorf("assignee ID was somehow set to: %d", *req.AssigneeID)
 	}
-	if _, ok := i.(api.Identifier); !ok {
-		t.Errorf("DeliveryServiceRequest must be Identifier")
+	if req.Assignee != nil {
+		t.Errorf("assignee was somehow set to: %s", *req.Assignee)
 	}
-	if _, ok := i.(api.Tenantable); !ok {
-		t.Errorf("DeliveryServiceRequest must be Tenantable")
+
+	expectID := 12
+	expectName := "test assignee"
+
+	req.Assignee = &expectName
+
+	rows := sqlmock.NewRows([]string{"id"})
+	rows.AddRow(expectID)
+	mock.ExpectBegin()
+	mock.ExpectQuery("SELECT id").WillReturnRows(rows)
+
+	// check case where getting Assignee ID from username
+	_, _, userErr, sysErr = getAssignee(&req, "test", db.MustBegin().Tx)
+	if userErr != nil {
+		t.Errorf("unexpected user error: %v", userErr)
+	}
+	if sysErr != nil {
+		t.Errorf("unexpected system error: %v", sysErr)
 	}
-}
 
-func TestGetDeliveryServiceRequest(t *testing.T) {
-	s := "this is not a valid xmlid.  Bad characters and too long."
-	i := 1
-	b := true
-	u := "UPDATE"
-	st := tc.RequestStatusSubmitted
-	ds := tc.DeliveryServiceV4{}
-	ds.XMLID = &s
-	ds.CDNID = &i
-	ds.LogsEnabled = &b
-	ds.DSCP = nil
-	ds.GeoLimit = &i
-	ds.Active = &b
-	ds.TypeID = &i
-	r := &TODeliveryServiceRequest{DeliveryServiceRequestNullable: tc.DeliveryServiceRequestNullable{
-		ChangeType:      &u,
-		Status:          &st,
-		DeliveryService: &ds,
-	}}
-
-	expectedErrors := []string{
-		/*
-			`'regionalGeoBlocking' is required`,
-			`'xmlId' cannot contain spaces`,
-			`'dscp' is required`,
-			`'displayName' cannot be blank`,
-			`'geoProvider' is required`,
-			`'typeId' is required`,
-		*/
-	}
-
-	r.SetKeys(map[string]interface{}{"id": 10})
-	keys, _ := r.GetKeys()
-	if keys["id"].(int) != 10 {
-		t.Errorf("expected ID to be %d,  not %d", 10, keys["id"].(int))
-	}
-	exp := "10"
-	if s != r.GetAuditName() {
-		t.Errorf("expected AuditName to be '%s',  not '%s'", s, r.GetAuditName())
-	}
-	exp = "deliveryservice_request"
-	if r.GetType() != "deliveryservice_request" {
-		t.Errorf("expected Type to be %s,  not %s", exp, r.GetType())
-	}
-
-	var errs []error
-	mockDB, _, err := sqlmock.New()
-	if err != nil {
-		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	if req.Assignee == nil {
+		t.Error("Expected assignee to not be nil after getting assignee")
+	} else if *req.Assignee != expectName {
+		t.Errorf("Incorrect assignee; expected: '%s', got: '%s'", expectName, *req.Assignee)
 	}
-	defer mockDB.Close()
 
-	db := sqlx.NewDb(mockDB, "sqlmock")
-	defer db.Close()
+	if req.AssigneeID == nil {
+		t.Error("Expected assignee ID to not be nil after getting assignee")
+	} else if *req.AssigneeID != expectID {
+		t.Errorf("Incorrect assignee ID; expected: %d, got: %d", expectID, *req.AssigneeID)
+	}
 
-	/* TODO: this section panics when deliveryservice.Validate() tries to get the type name.
-	q := `insert into type (name, description, use_in_table) values ('HTTP', 'HTTP Content Routing', 'deliveryservice') ON CONFLICT (name) DO NOTHING;`
-	qe := `insert into type \(name, description, use_in_table\) values \('HTTP', 'HTTP Content Routing', 'deliveryservice'\) ON CONFLICT \(name\) DO NOTHING;`
-	mock.ExpectExec(qe).WillReturnResult(sqlmock.NewResult(1, 1))
-	res, err := db.Exec(q)
+	req.Assignee = nil
+	req.AssigneeID = &expectID
 
-	// db.Exec(`insert into type (name, description, use_in_table) values ('HTTP_NO_CACHE', 'HTTP Content Routing, no caching', 'deliveryservice') ON CONFLICT (name) DO NOTHING;`)
-	//db.Exec(`insert into type (name, description, use_in_table) values ('HTTP_LIVE', 'HTTP Content routing cache in RAM', 'deliveryservice') ON CONFLICT (name) DO NOTHING;`)
-	if err != nil {
-		t.Error(err)
+	rows = sqlmock.NewRows([]string{"username"})
+	rows.AddRow(expectName)
+	mock.ExpectBegin()
+	mock.ExpectQuery("SELECT username").WillReturnRows(rows)
+
+	// check case where getting username from Assignee ID
+	_, _, userErr, sysErr = getAssignee(&req, "test", db.MustBegin().Tx)
+	if userErr != nil {
+		t.Errorf("unexpected user error: %v", userErr)
+	}
+	if sysErr != nil {
+		t.Errorf("unexpected system error: %v", sysErr)
 	}
-	mock.ExpectQuery(`SELECT name from type where id=\$1`).WillReturnRows(sqlmock.NewRows([]string{"name"}))
 
-	errs := r.Validate(db)
-	*/
-	if len(errs) != len(expectedErrors) {
-		for _, e := range errs {
-			t.Error(e)
-		}
+	if req.Assignee == nil {
+		t.Error("Expected assignee to not be nil after getting assignee")
+	} else if *req.Assignee != expectName {
+		t.Errorf("Incorrect assignee; expected: '%s', got: '%s'", expectName, *req.Assignee)
 	}
 
-	for e := range expectedErrors {
-		t.Error(e)
+	if req.AssigneeID == nil {
+		t.Error("Expected assignee ID to not be nil after getting assignee")
+	} else if *req.AssigneeID != expectID {
+		t.Errorf("Incorrect assignee ID; expected: %d, got: %d", expectID, *req.AssigneeID)
 	}
 
-	/*
-		if r.Update(db *sqlx.Tx, ctx context.Context) {
-			t.Errorf("expected ID to be %d,  not %d", 10, r.GetID())
-		}
-		if r.Insert(db *sqlx.Tx, ctx context.Context) {
-			t.Errorf("expected ID to be %d,  not %d", 10, r.GetID())
-		}
-		if r.Delete(db *sqlx.Tx, ctx context.Context) {
-			t.Errorf("expected ID to be %d,  not %d", 10, r.GetID())
-		}
-	*/
+	req.Assignee = new(string)
+	*req.Assignee = expectName + " - but not actually"
+	req.AssigneeID = &expectID
+	rows = sqlmock.NewRows([]string{"username"})
+	rows.AddRow(expectName)
+	mock.ExpectBegin()
+	mock.ExpectQuery("SELECT username").WillReturnRows(rows)
+
+	// check that Assignee ID has precedence over Assignee
+	_, _, userErr, sysErr = getAssignee(&req, "test", db.MustBegin().Tx)
+	if userErr != nil {
+		t.Errorf("unexpected user error: %v", userErr)
+	}
+	if sysErr != nil {
+		t.Errorf("unexpected system error: %v", sysErr)
+	}
+
+	if req.Assignee == nil {
+		t.Error("Expected assignee to not be nil after getting assignee")
+	} else if *req.Assignee != expectName {
+		t.Errorf("Incorrect assignee; expected: '%s', got: '%s'", expectName, *req.Assignee)
+	}
+
+	if req.AssigneeID == nil {
+		t.Error("Expected assignee ID to not be nil after getting assignee")
+	} else if *req.AssigneeID != expectID {
+		t.Errorf("Incorrect assignee ID; expected: %d, got: %d", expectID, *req.AssigneeID)
+	}
+
+	req.Assignee = nil
+	req.AssigneeID = &expectID
+	rows = sqlmock.NewRows([]string{"username"})
+	mock.ExpectBegin()
+	mock.ExpectQuery("SELECT username").WillReturnRows(rows)
+
+	// check that looking for ID of non-existent username is an error
+	_, _, userErr, sysErr = getAssignee(&req, "test", db.MustBegin().Tx)
+	if userErr == nil {
+		t.Error("Expected a user error, but didn't get one")
+	}
+
+	req.Assignee = &expectName
+	req.AssigneeID = nil
+	rows = sqlmock.NewRows([]string{"id"})
+	mock.ExpectBegin()
+	mock.ExpectQuery("SELECT id").WillReturnRows(rows)
+
+	// check that looking for username of non-existent Assignee is an error
+	_, _, userErr, sysErr = getAssignee(&req, "test", db.MustBegin().Tx)
+	if userErr == nil {
+		t.Error("Expected a user error, but didn't get one")
+	}
 }
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/status.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/status.go
new file mode 100644
index 0000000..71c9b0d
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/status.go
@@ -0,0 +1,180 @@
+package request
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/routing/middleware"
+)
+
+// GetStatus is the handler for GET requests to
+// /deliveryservice_requests/{{ID}}/status.
+func GetStatus(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	// Middleware should've already handled this, so idk why this is a pointer at all tbh
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	// This should never happen because a route doesn't exist for it
+	if version.Major < 4 {
+		w.Header().Set("Allow", http.MethodPut)
+		w.WriteHeader(http.StatusMethodNotAllowed)
+		api.WriteRespAlert(w, r, tc.ErrorLevel, http.StatusText(http.StatusMethodNotAllowed))
+		return
+	}
+
+	var dsr tc.DeliveryServiceRequestV40
+	if err := inf.Tx.QueryRowx(selectQuery+"WHERE r.id=$1", inf.IntParams["id"]).StructScan(&dsr); err != nil {
+		if err == sql.ErrNoRows {
+			errCode = http.StatusNotFound
+			userErr = fmt.Errorf("no such Delivery Service Request: %d", inf.IntParams["id"])
+			sysErr = nil
+		} else {
+			errCode = http.StatusInternalServerError
+			userErr = nil
+			sysErr = fmt.Errorf("looking for DSR: %v", err)
+		}
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+
+	authorized, err := isTenantAuthorized(dsr, inf)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	if !authorized {
+		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
+	}
+
+	api.WriteResp(w, r, dsr.Status)
+}
+
+const updateStatusQuery = `
+UPDATE deliveryservice_request
+SET status = $1, last_edited_by_id = $2
+WHERE id = $3
+RETURNING last_updated
+`
+
+// PutStatus is the handler for PUT requests to
+// /deliveryservice_requests/{{ID}}/status.
+func PutStatus(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	// Middleware should've already handled this, so idk why this is a pointer at all tbh
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if inf.User == nil {
+		sysErr = errors.New("got api info with no user")
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr)
+		return
+	}
+
+	var req tc.StatusChangeRequest
+	if err := api.Parse(r.Body, tx, &req); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+
+	dsrID := inf.IntParams["id"]
+
+	var dsr tc.DeliveryServiceRequestV40
+	if err := inf.Tx.QueryRowx(selectQuery+"WHERE r.id=$1", dsrID).StructScan(&dsr); err != nil {
+		if err == sql.ErrNoRows {
+			errCode = http.StatusNotFound
+			userErr = fmt.Errorf("no such Delivery Service Request: %d", dsrID)
+			sysErr = nil
+		} else {
+			errCode = http.StatusInternalServerError
+			userErr = nil
+			sysErr = fmt.Errorf("looking for DSR: %v", err)
+		}
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	dsr.SetXMLID()
+
+	if err := dsr.Status.ValidTransition(req.Status); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+
+	authorized, err := isTenantAuthorized(dsr, inf)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	if !authorized {
+		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
+	}
+
+	dsr.LastEditedBy = inf.User.UserName
+	dsr.LastEditedByID = new(int)
+	*dsr.LastEditedByID = inf.User.ID
+
+	if err := tx.QueryRow(updateStatusQuery, req.Status, inf.User.ID, dsrID).Scan(&dsr.LastUpdated); err != nil {
+		sysErr = fmt.Errorf("updating DSR #%d status: %v", dsrID, err)
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	message := fmt.Sprintf("Changed status of '%s' Delivery Service Request from '%s' to '%s'", dsr.XMLID, dsr.Status, req.Status)
+	dsr.Status = req.Status
+
+	var resp interface{}
+	if inf.Version.Major >= 4 {
+		resp = dsr
+	} else {
+		resp = dsr.Downgrade()
+	}
+
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, message, resp)
+	message = fmt.Sprintf("Delivery Service Request: %d, ID: %d, ACTION: %s deliveryservice_request, keys: {id:%d }", *dsr.ID, *dsr.ID, message, *dsr.ID)
+	inf.CreateChangeLog(message)
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go
index c72ac60..331ca8f 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/validate.go
@@ -20,26 +20,33 @@ package request
  */
 
 import (
+	"database/sql"
 	"errors"
 	"fmt"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice"
-	"strconv"
 
+	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
 	"github.com/apache/trafficcontrol/lib/go-util"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice"
 
-	"github.com/go-ozzo/ozzo-validation"
+	validation "github.com/go-ozzo/ozzo-validation"
 )
 
-// Validate ensures all required fields are present and in correct form.  Also checks request JSON is complete and valid
-func (req *TODeliveryServiceRequest) Validate() error {
+// validateLegacy ensures all required fields are present and in correct form.
+// Also checks request JSON is complete and valid.
+func validateLegacy(dsr tc.DeliveryServiceRequestNullable, tx *sql.Tx) error {
+	if tx == nil {
+		log.Errorln("validating a legacy Delivery Service Request: nil transaction was passed")
+	}
+
 	fromStatus := tc.RequestStatusDraft
-	if req.ID != nil && *req.ID > 0 {
-		err := req.APIInfo().Tx.Tx.QueryRow(`SELECT status FROM deliveryservice_request WHERE id=` + strconv.Itoa(*req.ID)).Scan(&fromStatus)
+	if dsr.ID != nil && *dsr.ID > 0 {
+		err := tx.QueryRow(`SELECT status FROM deliveryservice_request WHERE id=$1`, *dsr.ID).Scan(&fromStatus)
 
 		if err != nil {
-			return err
+			log.Errorf("querying for dsr by ID %d: %v", *dsr.ID, err)
+			return errors.New("unknown error")
 		}
 	}
 
@@ -55,15 +62,82 @@ func (req *TODeliveryServiceRequest) Validate() error {
 	}
 
 	errMap := validation.Errors{
-		"changeType":      validation.Validate(req.ChangeType, validation.Required),
-		"deliveryservice": validation.Validate(req.DeliveryService, validation.Required),
-		"status":          validation.Validate(req.Status, validation.Required, validation.By(validTransition)),
+		"changeType":      validation.Validate(dsr.ChangeType, validation.Required),
+		"deliveryservice": validation.Validate(dsr.DeliveryService, validation.Required),
+		"status":          validation.Validate(dsr.Status, validation.Required, validation.By(validTransition)),
 	}
 	errs := tovalidate.ToErrors(errMap)
 	// ensure the deliveryservice requested is valid
-	e := deliveryservice.Validate(req.APIInfo().Tx.Tx, req.DeliveryService)
+	upgraded := dsr.DeliveryService.UpgradeToV4()
+	e := deliveryservice.Validate(tx, &upgraded)
 
 	errs = append(errs, e)
 
 	return util.JoinErrs(errs)
 }
+
+// validateV4 validates a DSR, returning - in order - a user-facing error that
+// should be shown to the client, and a system error.
+func validateV4(dsr tc.DeliveryServiceRequestV40, tx *sql.Tx) (error, error) {
+	if tx == nil {
+		return nil, errors.New("nil transaction")
+	}
+
+	fromStatus := tc.RequestStatusDraft
+	if dsr.ID != nil && *dsr.ID > 0 {
+		if err := tx.QueryRow(`SELECT status FROM deliveryservice_request WHERE id=$1`, *dsr.ID).Scan(&fromStatus); err != nil {
+			return nil, err
+		}
+	}
+
+	err := validation.ValidateStruct(&dsr,
+		validation.Field(&dsr.ChangeType, validation.Required),
+		validation.Field(&dsr.DeliveryService, validation.Required),
+		validation.Field(&dsr.Status, validation.By(
+			func(s interface{}) error {
+				if s == nil {
+					return errors.New("required")
+				}
+				toStatus, ok := s.(tc.RequestStatus)
+				if !ok {
+					return fmt.Errorf("expected RequestStatus type, got %T", s)
+				}
+				return fromStatus.ValidTransition(toStatus)
+			},
+		)),
+		validation.Field(&dsr.Assignee, validation.By(
+			func(a interface{}) error {
+				if a == nil {
+					return nil
+				}
+				assignee, ok := a.(*string)
+				if !ok {
+					return fmt.Errorf("expected string, got %T", a)
+				}
+				if assignee == nil {
+					return nil
+				}
+				var id int
+				if err := tx.QueryRow(`SELECT id FROM tm_user WHERE username=$1`, *assignee).Scan(&id); err != nil {
+					if err == sql.ErrNoRows {
+						return fmt.Errorf("no such user '%s'", *assignee)
+					}
+					// TODO: allow ParseValidators to return system errors?
+					return errors.New("unknown error")
+				}
+				dsr.AssigneeID = new(int)
+				*dsr.AssigneeID = id
+				return nil
+			},
+		)),
+	)
+	if err != nil {
+		return err, nil
+	}
+
+	if err = deliveryservice.Validate(tx, dsr.DeliveryService); err != nil {
+		err = fmt.Errorf("deliveryService: %v", err)
+	}
+
+	return err, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index d416637..b0b7b6f 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -370,14 +370,16 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{api.Version{4, 0}, http.MethodDelete, `cdns/{id}$`, api.DeleteHandler(&cdn.TOCDN{}), auth.PrivLevelOperations, Authenticated, nil, 4276946573},
 
 		//Delivery service requests
-		{api.Version{4, 0}, http.MethodGet, `deliveryservice_requests/?$`, api.ReadHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelReadOnly, Authenticated, nil, 46811639353},
-		{api.Version{4, 0}, http.MethodPut, `deliveryservice_requests/?$`, api.UpdateHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelPortal, Authenticated, nil, 42499079183},
-		{api.Version{4, 0}, http.MethodPost, `deliveryservice_requests/?$`, api.CreateHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelPortal, Authenticated, nil, 493850393},
-		{api.Version{4, 0}, http.MethodDelete, `deliveryservice_requests/?$`, api.DeleteHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelPortal, Authenticated, nil, 42969850253},
+		{api.Version{4, 0}, http.MethodGet, `deliveryservice_requests/?$`, dsrequest.Get, auth.PrivLevelReadOnly, Authenticated, nil, 46811639353},
+		{api.Version{4, 0}, http.MethodPut, `deliveryservice_requests/?$`, dsrequest.Put, auth.PrivLevelPortal, Authenticated, nil, 42499079183},
+		{api.Version{4, 0}, http.MethodPost, `deliveryservice_requests/?$`, dsrequest.Post, auth.PrivLevelPortal, Authenticated, nil, 493850393},
+		{api.Version{4, 0}, http.MethodDelete, `deliveryservice_requests/?$`, dsrequest.Delete, auth.PrivLevelPortal, Authenticated, nil, 42969850253},
 
 		//Delivery service request: Actions
-		{api.Version{4, 0}, http.MethodPut, `deliveryservice_requests/{id}/assign$`, api.UpdateHandler(dsrequest.GetAssignmentSingleton()), auth.PrivLevelOperations, Authenticated, nil, 47031602903},
-		{api.Version{4, 0}, http.MethodPut, `deliveryservice_requests/{id}/status$`, api.UpdateHandler(dsrequest.GetStatusSingleton()), auth.PrivLevelPortal, Authenticated, nil, 4684150993},
+		{api.Version{4, 0}, http.MethodGet, `deliveryservice_requests/{id}/assign$`, dsrequest.GetAssignment, auth.PrivLevelOperations, Authenticated, nil, 47031602904},
+		{api.Version{4, 0}, http.MethodPut, `deliveryservice_requests/{id}/assign$`, dsrequest.PutAssignment, auth.PrivLevelOperations, Authenticated, nil, 47031602903},
+		{api.Version{4, 0}, http.MethodGet, `deliveryservice_requests/{id}/status$`, dsrequest.GetStatus, auth.PrivLevelPortal, Authenticated, nil, 4684150994},
+		{api.Version{4, 0}, http.MethodPut, `deliveryservice_requests/{id}/status$`, dsrequest.PutStatus, auth.PrivLevelPortal, Authenticated, nil, 4684150993},
 
 		//Delivery service request comment: CRUD
 		{api.Version{4, 0}, http.MethodGet, `deliveryservice_request_comments/?$`, api.ReadHandler(&comment.TODeliveryServiceRequestComment{}), auth.PrivLevelReadOnly, Authenticated, nil, 40326507373},
@@ -757,14 +759,14 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{api.Version{3, 0}, http.MethodDelete, `cdns/{id}$`, api.DeleteHandler(&cdn.TOCDN{}), auth.PrivLevelOperations, Authenticated, nil, 2276946573},
 
 		//Delivery service requests
-		{api.Version{3, 0}, http.MethodGet, `deliveryservice_requests/?$`, api.ReadHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelReadOnly, Authenticated, nil, 26811639353},
-		{api.Version{3, 0}, http.MethodPut, `deliveryservice_requests/?$`, api.UpdateHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelPortal, Authenticated, nil, 22499079183},
-		{api.Version{3, 0}, http.MethodPost, `deliveryservice_requests/?$`, api.CreateHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelPortal, Authenticated, nil, 293850393},
-		{api.Version{3, 0}, http.MethodDelete, `deliveryservice_requests/?$`, api.DeleteHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelPortal, Authenticated, nil, 22969850253},
+		{api.Version{3, 0}, http.MethodGet, `deliveryservice_requests/?$`, dsrequest.Get, auth.PrivLevelReadOnly, Authenticated, nil, 26811639353},
+		{api.Version{3, 0}, http.MethodPut, `deliveryservice_requests/?$`, dsrequest.Put, auth.PrivLevelPortal, Authenticated, nil, 22499079183},
+		{api.Version{3, 0}, http.MethodPost, `deliveryservice_requests/?$`, dsrequest.Post, auth.PrivLevelPortal, Authenticated, nil, 293850393},
+		{api.Version{3, 0}, http.MethodDelete, `deliveryservice_requests/?$`, dsrequest.Delete, auth.PrivLevelPortal, Authenticated, nil, 22969850253},
 
 		//Delivery service request: Actions
-		{api.Version{3, 0}, http.MethodPut, `deliveryservice_requests/{id}/assign$`, api.UpdateHandler(dsrequest.GetAssignmentSingleton()), auth.PrivLevelOperations, Authenticated, nil, 27031602903},
-		{api.Version{3, 0}, http.MethodPut, `deliveryservice_requests/{id}/status$`, api.UpdateHandler(dsrequest.GetStatusSingleton()), auth.PrivLevelPortal, Authenticated, nil, 2684150993},
+		{api.Version{3, 0}, http.MethodPut, `deliveryservice_requests/{id}/assign$`, dsrequest.PutAssignment, auth.PrivLevelOperations, Authenticated, nil, 27031602903},
+		{api.Version{3, 0}, http.MethodPut, `deliveryservice_requests/{id}/status$`, dsrequest.PutStatus, auth.PrivLevelPortal, Authenticated, nil, 2684150993},
 
 		//Delivery service request comment: CRUD
 		{api.Version{3, 0}, http.MethodGet, `deliveryservice_request_comments/?$`, api.ReadHandler(&comment.TODeliveryServiceRequestComment{}), auth.PrivLevelReadOnly, Authenticated, nil, 20326507373},
@@ -1127,14 +1129,14 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{api.Version{2, 0}, http.MethodDelete, `cdns/{id}$`, api.DeleteHandler(&cdn.TOCDN{}), auth.PrivLevelOperations, Authenticated, nil, 227694657},
 
 		//Delivery service requests
-		{api.Version{2, 0}, http.MethodGet, `deliveryservice_requests/?$`, api.ReadHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelReadOnly, Authenticated, nil, 2681163935},
-		{api.Version{2, 0}, http.MethodPut, `deliveryservice_requests/?$`, api.UpdateHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelPortal, Authenticated, nil, 2249907918},
-		{api.Version{2, 0}, http.MethodPost, `deliveryservice_requests/?$`, api.CreateHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelPortal, Authenticated, nil, 29385039},
-		{api.Version{2, 0}, http.MethodDelete, `deliveryservice_requests/?$`, api.DeleteHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelPortal, Authenticated, nil, 2296985025},
+		{api.Version{2, 0}, http.MethodGet, `deliveryservice_requests/?$`, dsrequest.Get, auth.PrivLevelReadOnly, Authenticated, nil, 2681163935},
+		{api.Version{2, 0}, http.MethodPut, `deliveryservice_requests/?$`, dsrequest.Put, auth.PrivLevelPortal, Authenticated, nil, 2249907918},
+		{api.Version{2, 0}, http.MethodPost, `deliveryservice_requests/?$`, dsrequest.Post, auth.PrivLevelPortal, Authenticated, nil, 29385039},
+		{api.Version{2, 0}, http.MethodDelete, `deliveryservice_requests/?$`, dsrequest.Delete, auth.PrivLevelPortal, Authenticated, nil, 2296985025},
 
 		//Delivery service request: Actions
-		{api.Version{2, 0}, http.MethodPut, `deliveryservice_requests/{id}/assign$`, api.UpdateHandler(dsrequest.GetAssignmentSingleton()), auth.PrivLevelOperations, Authenticated, nil, 2703160290},
-		{api.Version{2, 0}, http.MethodPut, `deliveryservice_requests/{id}/status$`, api.UpdateHandler(dsrequest.GetStatusSingleton()), auth.PrivLevelPortal, Authenticated, nil, 268415099},
+		{api.Version{2, 0}, http.MethodPut, `deliveryservice_requests/{id}/assign$`, dsrequest.PutAssignment, auth.PrivLevelOperations, Authenticated, nil, 2703160290},
+		{api.Version{2, 0}, http.MethodPut, `deliveryservice_requests/{id}/status$`, dsrequest.PutStatus, auth.PrivLevelPortal, Authenticated, nil, 268415099},
 
 		//Delivery service request comment: CRUD
 		{api.Version{2, 0}, http.MethodGet, `deliveryservice_request_comments/?$`, api.ReadHandler(&comment.TODeliveryServiceRequestComment{}), auth.PrivLevelReadOnly, Authenticated, nil, 2032650737},
@@ -1529,15 +1531,15 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{api.Version{1, 3}, http.MethodDelete, `asns/?$`, api.DeleteHandler(&asn.TOASNV11{}), auth.PrivLevelOperations, Authenticated, nil, 680204898},
 
 		//Delivery service requests
-		{api.Version{1, 3}, http.MethodGet, `deliveryservice_requests/?(\.json)?$`, api.ReadHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelReadOnly, Authenticated, nil, 1681163935},
-		{api.Version{1, 3}, http.MethodGet, `deliveryservice_requests/?$`, api.ReadHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelReadOnly, Authenticated, nil, 286812311},
-		{api.Version{1, 3}, http.MethodPut, `deliveryservice_requests/?$`, api.UpdateHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelPortal, Authenticated, nil, 2049907918},
-		{api.Version{1, 3}, http.MethodPost, `deliveryservice_requests/?$`, api.CreateHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelPortal, Authenticated, nil, 59385039},
-		{api.Version{1, 3}, http.MethodDelete, `deliveryservice_requests/?$`, api.DeleteHandler(&dsrequest.TODeliveryServiceRequest{}), auth.PrivLevelPortal, Authenticated, nil, 1296985025},
+		{api.Version{1, 3}, http.MethodGet, `deliveryservice_requests/?(\.json)?$`, dsrequest.Get, auth.PrivLevelReadOnly, Authenticated, nil, 1681163935},
+		{api.Version{1, 3}, http.MethodGet, `deliveryservice_requests/?$`, dsrequest.Get, auth.PrivLevelReadOnly, Authenticated, nil, 286812311},
+		{api.Version{1, 3}, http.MethodPut, `deliveryservice_requests/?$`, dsrequest.Put, auth.PrivLevelPortal, Authenticated, nil, 2049907918},
+		{api.Version{1, 3}, http.MethodPost, `deliveryservice_requests/?$`, dsrequest.Post, auth.PrivLevelPortal, Authenticated, nil, 59385039},
+		{api.Version{1, 3}, http.MethodDelete, `deliveryservice_requests/?$`, dsrequest.Delete, auth.PrivLevelPortal, Authenticated, nil, 1296985025},
 
 		//Delivery service request: Actions
-		{api.Version{1, 3}, http.MethodPut, `deliveryservice_requests/{id}/assign$`, api.UpdateHandler(dsrequest.GetAssignmentSingleton()), auth.PrivLevelOperations, Authenticated, nil, 1703160290},
-		{api.Version{1, 3}, http.MethodPut, `deliveryservice_requests/{id}/status$`, api.UpdateHandler(dsrequest.GetStatusSingleton()), auth.PrivLevelPortal, Authenticated, nil, 668415099},
+		{api.Version{1, 3}, http.MethodPut, `deliveryservice_requests/{id}/assign$`, dsrequest.PutAssignment, auth.PrivLevelOperations, Authenticated, nil, 1703160290},
+		{api.Version{1, 3}, http.MethodPut, `deliveryservice_requests/{id}/status$`, dsrequest.PutStatus, auth.PrivLevelPortal, Authenticated, nil, 668415099},
 
 		//Delivery service request comment: CRUD
 		{api.Version{1, 3}, http.MethodGet, `deliveryservice_request_comments/?(\.json)?$`, api.ReadHandler(&comment.TODeliveryServiceRequestComment{}), auth.PrivLevelReadOnly, Authenticated, nil, 1032650737},
diff --git a/traffic_ops/traffic_ops_golang/util/ims/ims.go b/traffic_ops/traffic_ops_golang/util/ims/ims.go
index b6c32e9..53dbbbd 100644
--- a/traffic_ops/traffic_ops_golang/util/ims/ims.go
+++ b/traffic_ops/traffic_ops_golang/util/ims/ims.go
@@ -30,12 +30,13 @@ import (
  * under the License.
  */
 
-// LatestTimestamp to keep track of the max of "last updated" times in tables
+// LatestTimestamp to keep track of the max of "last updated" times in tables.
 type LatestTimestamp struct {
 	LatestTime *tc.TimeNoMod `json:"latestTime" db:"max"`
 }
 
-// TryIfModifiedSinceQuery for components that DO NOT implement the CRUDER interface
+// TryIfModifiedSinceQuery for components that DO NOT implement the CRUDER
+// interface(s).
 // Checks to see the max time that an entity was changed, and then returns a boolean (which tells us whether or not to run the main query for the endpoint)
 // along with the max time
 // If the returned boolean is false, there is no need to run the main query for the GET API endpoint, and we return a 304 status
diff --git a/traffic_ops/v3-client/session.go b/traffic_ops/v3-client/session.go
index 174b91d..cedbe2e 100644
--- a/traffic_ops/v3-client/session.go
+++ b/traffic_ops/v3-client/session.go
@@ -17,8 +17,14 @@
 package client
 
 import (
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"io/ioutil"
 	"net"
 	"net/http"
+	"strconv"
+	"strings"
 	"time"
 
 	"github.com/apache/trafficcontrol/traffic_ops/toclientlib"
@@ -89,7 +95,94 @@ func LogoutWithAgent(toURL string, toUser string, toPasswd string, insecure bool
 // this can be used for querying unauthenticated endpoints without requiring a login
 // The useCache argument is ignored. It exists to avoid breaking compatibility, and does not exist in newer functions.
 func NewNoAuthSession(toURL string, insecure bool, userAgent string, useCache bool, requestTimeout time.Duration) *Session {
-	return &Session{TOClient: *toclientlib.NewNoAuthClient(toURL, insecure, userAgent, requestTimeout, apiVersions())}
+	return NewSession("", "", toURL, userAgent, &http.Client{
+		Timeout: requestTimeout,
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
+		},
+	}, useCache)
+}
+
+// ErrIsNotImplemented checks if err ultimately arose at least in part because
+// the Traffic Ops server did not support the requested API version.
+func ErrIsNotImplemented(err error) bool {
+	return err != nil && strings.Contains(err.Error(), ErrNotImplemented.Error()) // use string.Contains in case context was added to the error
+}
+
+// ErrNotImplemented is returned when Traffic Ops returns a 501 Not Implemented
+// Users should check ErrIsNotImplemented rather than comparing directly, in case context was added.
+var ErrNotImplemented = errors.New("Traffic Ops Server returned 'Not Implemented', this client is probably newer than Traffic Ops, and you probably need to either upgrade Traffic Ops, or use a client whose version matches your Traffic Ops version")
+
+// errUnlessOKOrNotModified returns the response, the remote address, and an error if the given Response's status code is anything
+// but 200 OK/ 304 Not Modified. This includes reading the Response.Body and Closing it. Otherwise, the given response, the remote
+// address, and a nil error are returned.
+func (to *Session) errUnlessOKOrNotModified(resp *http.Response, remoteAddr net.Addr, err error, path string) (*http.Response, net.Addr, error) {
+	if err != nil {
+		return resp, remoteAddr, err
+	}
+	if resp.StatusCode < 300 || resp.StatusCode == 304 {
+		return resp, remoteAddr, err
+	}
+
+	defer resp.Body.Close()
+
+	if resp.StatusCode == http.StatusNotImplemented {
+		return resp, remoteAddr, ErrNotImplemented
+	}
+
+	body, readErr := ioutil.ReadAll(resp.Body)
+	if readErr != nil {
+		return resp, remoteAddr, readErr
+	}
+	return resp, remoteAddr, errors.New(resp.Status + "[" + strconv.Itoa(resp.StatusCode) + "] - Error requesting Traffic Ops " + to.getURL(path) + " " + string(body))
+}
+
+func (to *Session) getURL(path string) string { return to.URL + path }
+
+type ReqF func(to *Session, method string, path string, body interface{}, header http.Header, response interface{}) (toclientlib.ReqInf, error)
+
+type MidReqF func(ReqF) ReqF
+
+// composeReqFuncs takes an initial request func and middleware, and
+// returns a single ReqFunc to be called,
+func composeReqFuncs(reqF ReqF, middleware []MidReqF) ReqF {
+	// compose in reverse-order, which causes them to be applied in forward-order.
+	for i := len(middleware) - 1; i >= 0; i-- {
+		reqF = middleware[i](reqF)
+	}
+	return reqF
+}
+
+// makeRequestWithHeader marshals the response body (if non-nil), performs the HTTP request,
+// and decodes the response into the given response pointer.
+//
+// Note processing on the following codes:
+// 304 http.StatusNotModified  - Will return the 304 in ReqInf, a nil error, and a nil response.
+//
+// 401 http.StatusUnauthorized - Via to.request(), Same as 403 Forbidden.
+// 403 http.StatusForbidden    - Via to.request()
+//                               Will try to log in again, and try the request again.
+//                               The second request is returned, even if it fails.
+//
+// To request the bytes without deserializing, pass a *[]byte response.
+//
+func makeRequestWithHeader(to *Session, method, path string, body interface{}, header http.Header, response interface{}) (toclientlib.ReqInf, error) {
+	var remoteAddr net.Addr
+	reqInf := toclientlib.ReqInf{CacheHitStatus: toclientlib.CacheHitStatusMiss, RemoteAddr: remoteAddr}
+	var reqBody []byte
+	var err error
+	if body != nil {
+		reqBody, err = json.Marshal(body)
+		if err != nil {
+			return reqInf, errors.New("marshalling request body: " + err.Error())
+		}
+	}
+	reqInf, err = to.TOClient.Req(method, path, reqBody, header, &response)
+	reqInf.RemoteAddr = remoteAddr
+	if err != nil {
+		return reqInf, errors.New("requesting from Traffic Ops: " + err.Error())
+	}
+	return reqInf, nil
 }
 
 func (to *Session) get(path string, header http.Header, response interface{}) (toclientlib.ReqInf, error) {
diff --git a/traffic_ops/v4-client/deliveryservice_requests.go b/traffic_ops/v4-client/deliveryservice_requests.go
index ce0504c..b995d59 100644
--- a/traffic_ops/v4-client/deliveryservice_requests.go
+++ b/traffic_ops/v4-client/deliveryservice_requests.go
@@ -25,132 +25,140 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/toclientlib"
 )
 
-const (
-	APIDSRequests = "/deliveryservice_requests"
-)
+// APIDSRequests is the API version-relative path to the
+// /deliveryservice_requests API endpoint.
+const APIDSRequests = "/deliveryservice_requests"
 
-// CreateDeliveryServiceRequest creates a Delivery Service Request.
-func (to *Session) CreateDeliveryServiceRequest(dsr tc.DeliveryServiceRequest) (tc.Alerts, toclientlib.ReqInf, error) {
+// CreateDeliveryServiceRequest creates the given Delivery Service Request.
+func (to *Session) CreateDeliveryServiceRequest(dsr tc.DeliveryServiceRequestV40, hdr http.Header) (tc.DeliveryServiceRequestV40, tc.Alerts, toclientlib.ReqInf, error) {
 	var alerts tc.Alerts
-	if dsr.AssigneeID == 0 && dsr.Assignee != "" {
-		res, reqInf, err := to.GetUserByUsernameWithHdr(dsr.Assignee, nil)
+	var created tc.DeliveryServiceRequestV40
+	if dsr.AssigneeID == nil && dsr.Assignee != nil {
+		res, reqInf, err := to.GetUserByUsernameWithHdr(*dsr.Assignee, nil)
 		if err != nil {
-			return alerts, reqInf, err
+			return created, alerts, reqInf, err
 		}
 		if len(res) == 0 {
-			return alerts, reqInf, errors.New("no user with name " + dsr.Assignee)
+			return created, alerts, reqInf, fmt.Errorf("no user with username '%s'", *dsr.Assignee)
 		}
-		dsr.AssigneeID = *res[0].ID
+		dsr.AssigneeID = res[0].ID
 	}
 
-	if dsr.AuthorID == 0 && dsr.Author != "" {
+	if dsr.AuthorID == nil && dsr.Author != "" {
 		res, reqInf, err := to.GetUserByUsernameWithHdr(dsr.Author, nil)
 		if err != nil {
-			return alerts, reqInf, err
+			return created, alerts, reqInf, err
 		}
 		if len(res) == 0 {
-			return alerts, reqInf, errors.New("no user with name " + dsr.Author)
+			return created, alerts, reqInf, fmt.Errorf("no user with name '%s'", dsr.Author)
 		}
-		dsr.AuthorID = tc.IDNoMod(*res[0].ID)
+		dsr.AuthorID = res[0].ID
 	}
 
-	if dsr.DeliveryService.TypeID == 0 && dsr.DeliveryService.Type.String() != "" {
+	if dsr.DeliveryService.TypeID == nil && dsr.DeliveryService.Type.String() != "" {
 		ty, reqInf, err := to.GetTypeByNameWithHdr(dsr.DeliveryService.Type.String(), nil)
 		if err != nil || len(ty) == 0 {
-			return alerts, reqInf, errors.New("no type named " + dsr.DeliveryService.Type.String())
+			return created, alerts, reqInf, errors.New("no type named " + dsr.DeliveryService.Type.String())
 		}
-		dsr.DeliveryService.TypeID = ty[0].ID
+		dsr.DeliveryService.TypeID = &ty[0].ID
 	}
 
-	if dsr.DeliveryService.CDNID == 0 && dsr.DeliveryService.CDNName != "" {
-		cdns, reqInf, err := to.GetCDNByNameWithHdr(dsr.DeliveryService.CDNName, nil)
+	if dsr.DeliveryService.CDNID == nil && dsr.DeliveryService.CDNName != nil {
+		cdns, reqInf, err := to.GetCDNByNameWithHdr(*dsr.DeliveryService.CDNName, nil)
 		if err != nil || len(cdns) == 0 {
-			return alerts, reqInf, errors.New("no CDN named " + dsr.DeliveryService.CDNName)
+			return created, alerts, reqInf, fmt.Errorf("no CDN named '%s'", *dsr.DeliveryService.CDNName)
 		}
-		dsr.DeliveryService.CDNID = cdns[0].ID
+		dsr.DeliveryService.CDNID = &cdns[0].ID
 	}
 
-	if dsr.DeliveryService.ProfileID == 0 && dsr.DeliveryService.ProfileName != "" {
-		profiles, reqInf, err := to.GetProfileByNameWithHdr(dsr.DeliveryService.ProfileName, nil)
+	if dsr.DeliveryService.ProfileID == nil && dsr.DeliveryService.ProfileName != nil {
+		profiles, reqInf, err := to.GetProfileByNameWithHdr(*dsr.DeliveryService.ProfileName, nil)
 		if err != nil || len(profiles) == 0 {
-			return alerts, reqInf, errors.New("no Profile named " + dsr.DeliveryService.ProfileName)
+			return created, alerts, reqInf, fmt.Errorf("no Profile named '%s'", *dsr.DeliveryService.ProfileName)
 		}
-		dsr.DeliveryService.ProfileID = profiles[0].ID
+		dsr.DeliveryService.ProfileID = &profiles[0].ID
 	}
 
-	if dsr.DeliveryService.TenantID == 0 && dsr.DeliveryService.Tenant != "" {
-		ten, reqInf, err := to.TenantByNameWithHdr(dsr.DeliveryService.Tenant, nil)
+	if dsr.DeliveryService.TenantID == nil && dsr.DeliveryService.Tenant != nil {
+		ten, reqInf, err := to.TenantByNameWithHdr(*dsr.DeliveryService.Tenant, nil)
 		if err != nil || ten == nil {
-			return alerts, reqInf, errors.New("no Tenant named " + dsr.DeliveryService.Tenant)
+			return created, alerts, reqInf, fmt.Errorf("no Tenant named '%s'", *dsr.DeliveryService.Tenant)
 		}
-		dsr.DeliveryService.TenantID = ten.ID
+		dsr.DeliveryService.TenantID = &ten.ID
 	}
 
-	reqInf, err := to.post(APIDSRequests, dsr, nil, &alerts)
-	return alerts, reqInf, err
+	var resp struct {
+		tc.Alerts
+		Response tc.DeliveryServiceRequestV40 `json:"response"`
+	}
+	reqInf, err := to.post(APIDSRequests, dsr, nil, &resp)
+	alerts = resp.Alerts
+	created = resp.Response
+	return created, alerts, reqInf, err
 }
 
-func (to *Session) GetDeliveryServiceRequestsWithHdr(header http.Header) ([]tc.DeliveryServiceRequest, toclientlib.ReqInf, error) {
-	data := struct {
-		Response []tc.DeliveryServiceRequest `json:"response"`
-	}{}
+// GetDeliveryServiceRequests retrieves all Delivery Service Requests available to session user.
+func (to *Session) GetDeliveryServiceRequests(header http.Header) ([]tc.DeliveryServiceRequestV40, tc.Alerts, toclientlib.ReqInf, error) {
+	var data struct {
+		tc.Alerts
+		Response []tc.DeliveryServiceRequestV40 `json:"response"`
+	}
 	reqInf, err := to.get(APIDSRequests, header, &data)
-	return data.Response, reqInf, err
-}
-
-// GetDeliveryServiceRequests retrieves all deliveryservices available to session user.
-// Deprecated: GetDeliveryServiceRequests will be removed in 6.0. Use GetDeliveryServiceRequestsWithHdr.
-func (to *Session) GetDeliveryServiceRequests() ([]tc.DeliveryServiceRequest, toclientlib.ReqInf, error) {
-	return to.GetDeliveryServiceRequestsWithHdr(nil)
+	return data.Response, data.Alerts, reqInf, err
 }
 
-func (to *Session) GetDeliveryServiceRequestByXMLIDWithHdr(XMLID string, header http.Header) ([]tc.DeliveryServiceRequest, toclientlib.ReqInf, error) {
+// GetDeliveryServiceRequestsByXMLID retrives all Delivery Service Requests that
+// are requests to create, modify, or delete a Delivery Service with the given
+// XMLID.
+func (to *Session) GetDeliveryServiceRequestsByXMLID(XMLID string, header http.Header) ([]tc.DeliveryServiceRequestV40, tc.Alerts, toclientlib.ReqInf, error) {
 	route := fmt.Sprintf("%s?xmlId=%s", APIDSRequests, url.QueryEscape(XMLID))
-	data := struct {
-		Response []tc.DeliveryServiceRequest `json:"response"`
-	}{}
+	var data struct {
+		tc.Alerts
+		Response []tc.DeliveryServiceRequestV40 `json:"response"`
+	}
 	reqInf, err := to.get(route, header, &data)
-	return data.Response, reqInf, err
-}
-
-// GET a DeliveryServiceRequest by the DeliveryServiceRequest XMLID
-// Deprecated: GetDeliveryServiceRequestByXMLID will be removed in 6.0. Use GetDeliveryServiceRequestByXMLIDWithHdr.
-func (to *Session) GetDeliveryServiceRequestByXMLID(XMLID string) ([]tc.DeliveryServiceRequest, toclientlib.ReqInf, error) {
-	return to.GetDeliveryServiceRequestByXMLIDWithHdr(XMLID, nil)
+	return data.Response, data.Alerts, reqInf, err
 }
 
-func (to *Session) GetDeliveryServiceRequestByIDWithHdr(id int, header http.Header) ([]tc.DeliveryServiceRequest, toclientlib.ReqInf, error) {
+// GetDeliveryServiceRequest retrieves the Delivery Service Request with the given ID.
+func (to *Session) GetDeliveryServiceRequest(id int, header http.Header) (tc.DeliveryServiceRequestV40, tc.Alerts, toclientlib.ReqInf, error) {
 	route := fmt.Sprintf("%s?id=%d", APIDSRequests, id)
-	data := struct {
-		Response []tc.DeliveryServiceRequest `json:"response"`
-	}{}
+
+	var data struct {
+		tc.Alerts
+		Response []tc.DeliveryServiceRequestV40 `json:"response"`
+	}
 	reqInf, err := to.get(route, header, &data)
-	return data.Response, reqInf, err
-}
 
-// GET a DeliveryServiceRequest by the DeliveryServiceRequest id
-// Deprecated: GetDeliveryServiceRequestByID will be removed in 6.0. Use GetDeliveryServiceRequestByIDWithHdr.
-func (to *Session) GetDeliveryServiceRequestByID(id int) ([]tc.DeliveryServiceRequest, toclientlib.ReqInf, error) {
-	return to.GetDeliveryServiceRequestByIDWithHdr(id, nil)
+	// We presume the cases where an incorrect number of DSRs is returned will
+	// be captured in the error returned by to.get
+	var ret tc.DeliveryServiceRequestV40
+	if len(data.Response) == 1 {
+		ret = data.Response[0]
+	}
+
+	return ret, data.Alerts, reqInf, err
 }
 
-func (to *Session) UpdateDeliveryServiceRequestByIDWithHdr(id int, dsr tc.DeliveryServiceRequest, header http.Header) (tc.Alerts, toclientlib.ReqInf, error) {
+// DeleteDeliveryServiceRequest deletes the Delivery Service Request with the given ID.
+func (to *Session) DeleteDeliveryServiceRequest(id int) (tc.Alerts, toclientlib.ReqInf, error) {
 	route := fmt.Sprintf("%s?id=%d", APIDSRequests, id)
 	var alerts tc.Alerts
-	reqInf, err := to.put(route, dsr, header, &alerts)
+	reqInf, err := to.del(route, nil, &alerts)
 	return alerts, reqInf, err
 }
 
-// Update a DeliveryServiceRequest by ID
-// Deprecated: UpdateDeliveryServiceRequestByID will be removed in 6.0. Use UpdateDeliveryServiceRequestByIDWithHdr.
-func (to *Session) UpdateDeliveryServiceRequestByID(id int, dsr tc.DeliveryServiceRequest) (tc.Alerts, toclientlib.ReqInf, error) {
-	return to.UpdateDeliveryServiceRequestByIDWithHdr(id, dsr, nil)
-}
+// UpdateDeliveryServiceRequest replaces the existing DSR that has the given
+// ID with the DSR passed.
+func (to *Session) UpdateDeliveryServiceRequest(id int, dsr tc.DeliveryServiceRequestV4, header http.Header) (tc.DeliveryServiceRequestV4, tc.Alerts, toclientlib.ReqInf, error) {
 
-// DELETE a DeliveryServiceRequest by DeliveryServiceRequest assignee
-func (to *Session) DeleteDeliveryServiceRequestByID(id int) (tc.Alerts, toclientlib.ReqInf, error) {
 	route := fmt.Sprintf("%s?id=%d", APIDSRequests, id)
-	var alerts tc.Alerts
-	reqInf, err := to.del(route, nil, &alerts)
-	return alerts, reqInf, err
+
+	var payload struct {
+		tc.Alerts
+		Response tc.DeliveryServiceRequestV4 `json:"response"`
+	}
+	reqInf, err := to.put(route, dsr, header, &payload)
+
+	return payload.Response, payload.Alerts, reqInf, err
 }
diff --git a/traffic_portal/app/src/common/api/DeliveryServiceRequestService.js b/traffic_portal/app/src/common/api/DeliveryServiceRequestService.js
index e3591e8..763972d 100644
--- a/traffic_portal/app/src/common/api/DeliveryServiceRequestService.js
+++ b/traffic_portal/app/src/common/api/DeliveryServiceRequestService.js
@@ -74,8 +74,8 @@ var DeliveryServiceRequestService = function($http, locationUtils, messageModel,
 		);
 	};
 
-	this.assignDeliveryServiceRequest = function(id, userId) {
-		return $http.put(ENV.api['root'] + "deliveryservice_requests/" + id + "/assign", { id: id, assigneeId: userId }).then(
+	this.assignDeliveryServiceRequest = function(id, username) {
+		return $http.put(ENV.api['root'] + "deliveryservice_requests/" + id + "/assign", { assignee: username }).then(
 			function(result) {
 				return result;
 			},
@@ -87,7 +87,7 @@ var DeliveryServiceRequestService = function($http, locationUtils, messageModel,
 	};
 
 	this.updateDeliveryServiceRequestStatus = function(id, status) {
-		return $http.put(ENV.api['root'] + "deliveryservice_requests/" + id + "/status", { id: id, status: status }).then(
+		return $http.put(ENV.api['root'] + "deliveryservice_requests/" + id + "/status", { status: status }).then(
 			function(result) {
 				return result;
 			},
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/edit/FormEditDeliveryServiceController.js b/traffic_portal/app/src/common/modules/form/deliveryService/edit/FormEditDeliveryServiceController.js
index 26fa786..a838d48 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/edit/FormEditDeliveryServiceController.js
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/edit/FormEditDeliveryServiceController.js
@@ -80,7 +80,7 @@ var FormEditDeliveryServiceController = function(deliveryService, origin, topolo
 								function() {
 									var promises = [];
 									// assign the ds request
-									promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest(response.id, userModel.user.id));
+									promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest(response.id, userModel.user.username));
 									// set the status to 'complete'
 									promises.push(deliveryServiceRequestService.updateDeliveryServiceRequestStatus(response.id, 'complete'));
 								}
@@ -148,7 +148,7 @@ var FormEditDeliveryServiceController = function(deliveryService, origin, topolo
 
 					if (autoFulfilled) {
 						// assign the ds request
-						promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest(response.id, userModel.user.id));
+						promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest(response.id, userModel.user.username));
 						// set the status to 'submitted'
 						promises.push(deliveryServiceRequestService.updateDeliveryServiceRequestStatus(response.id, 'submitted'));
 					}
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/new/FormNewDeliveryServiceController.js b/traffic_portal/app/src/common/modules/form/deliveryService/new/FormNewDeliveryServiceController.js
index a38e337..d5ce8f4 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/new/FormNewDeliveryServiceController.js
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/new/FormNewDeliveryServiceController.js
@@ -53,7 +53,7 @@ var FormNewDeliveryServiceController = function(deliveryService, origin, topolog
 
 					if (autoFulfilled) {
 						// assign the ds request
-						promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest(response.id, userModel.user.id));
+						promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest(response.id, userModel.user.username));
 						// set the status to 'complete'
 						promises.push(deliveryServiceRequestService.updateDeliveryServiceRequestStatus(response.id, 'complete'));
 					}
diff --git a/traffic_portal/app/src/common/modules/table/deliveryServiceRequests/TableDeliveryServiceRequestsController.js b/traffic_portal/app/src/common/modules/table/deliveryServiceRequests/TableDeliveryServiceRequestsController.js
index 5559c4d..3e47ccf 100644
--- a/traffic_portal/app/src/common/modules/table/deliveryServiceRequests/TableDeliveryServiceRequestsController.js
+++ b/traffic_portal/app/src/common/modules/table/deliveryServiceRequests/TableDeliveryServiceRequestsController.js
@@ -377,8 +377,8 @@ var TableDeliveryServicesRequestsController = function (tableName, dsRequests, $
 			}
 		});
 		modalInstance.result.then(function () {
-			var assigneeId = (assign) ? userModel.user.id : null;
-			deliveryServiceRequestService.assignDeliveryServiceRequest(request.id, assigneeId).then(function () {
+			var assignee = (assign) ? userModel.user.username : null;
+			deliveryServiceRequestService.assignDeliveryServiceRequest(request.id, assignee).then(function () {
 				$scope.refresh();
 				if (assign) {
 					messageModel.setMessages([ { level: 'success', text: 'Delivery service request was assigned' } ], false);
diff --git a/traffic_portal/app/src/common/modules/table/deliveryServices/TableDeliveryServicesController.js b/traffic_portal/app/src/common/modules/table/deliveryServices/TableDeliveryServicesController.js
index 56ee8f2..e28fb93 100644
--- a/traffic_portal/app/src/common/modules/table/deliveryServices/TableDeliveryServicesController.js
+++ b/traffic_portal/app/src/common/modules/table/deliveryServices/TableDeliveryServicesController.js
@@ -533,7 +533,7 @@ var TableDeliveryServicesController = function(tableName, deliveryServices, filt
                                         function() {
                                             var promises = [];
                                             // assign the ds request
-                                            promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest(response.id, userModel.user.id));
+                                            promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest(response.id, userModel.user.username));
                                             // set the status to 'complete'
                                             promises.push(deliveryServiceRequestService.updateDeliveryServiceRequestStatus(response.id, 'complete'));
                                             // and finally refresh the delivery services table
diff --git a/traffic_portal/app/src/modules/private/deliveryServiceRequests/edit/FormEditDeliveryServiceRequestController.js b/traffic_portal/app/src/modules/private/deliveryServiceRequests/edit/FormEditDeliveryServiceRequestController.js
index 0e0c228..b4959c0 100644
--- a/traffic_portal/app/src/modules/private/deliveryServiceRequests/edit/FormEditDeliveryServiceRequestController.js
+++ b/traffic_portal/app/src/modules/private/deliveryServiceRequests/edit/FormEditDeliveryServiceRequestController.js
@@ -97,7 +97,7 @@ var FormEditDeliveryServiceRequestController = function(deliveryServiceRequest,
 			promises.push(deliveryServiceRequestService.updateDeliveryServiceRequest($scope.dsRequest.id, $scope.dsRequest));
 		}
 		// make sure the ds request is assigned to the user that is fulfilling the request
-		promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest($scope.dsRequest.id, userModel.user.id));
+		promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest($scope.dsRequest.id, userModel.user.username));
 		// set the status to 'pending'
 		promises.push(deliveryServiceRequestService.updateDeliveryServiceRequestStatus($scope.dsRequest.id, 'pending'));
 		return promises;