Repository: apache/trafficcontrol
Pull request #6835 (2022-05-16 17:59:05)

Fix t3c strategy peer yaml typo
Fixes a typo in cache config generation where strategies.yaml yaml peer merge keys were `<< ` instead of `<<: `, resulting in malformed yaml that ATS failed to load.

## Which Traffic Control components are affected by this PR?
- Traffic Control Cache Config (`t3c`, formerly ORT)

## What is the best way to verify this PR?
Run tests. Run t3c with strategies generated, verify ATS loads strategy file successfully.

## If this is a bugfix, which Traffic Control versions contained the bug?
Not in a release.


## PR submission checklist
- [x] This PR has tests <!-- If not, please delete this text and explain why this PR does not need tests. -->
- ~[x] This PR has documentation~ no docs, minor bug fix <!-- If not, please delete this text and explain why this PR does not need documentation. -->
- ~[x] This PR has a CHANGELOG.md entry~ no changelog, bug not in a release<!-- A fix for a bug from an ATC release, an improvement, or a new feature should have a changelog entry. -->
- [x] This PR **DOES NOT FIX A SERIOUS SECURITY VULNERABILITY** (see [the Apache Software Foundation's security guidelines](https://apache.org/security) for details)

<!--
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.
-->



Patch:
diff --git a/.github/workflows/ansible.molecule.fo.yml b/.github/workflows/ansible.molecule.fo.yml
index 69528495b..206212536 100644
--- a/.github/workflows/ansible.molecule.fo.yml
+++ b/.github/workflows/ansible.molecule.fo.yml
@@ -20,8 +20,6 @@ name: Molecule - FO
 on:
   create:
   push:
-    branches:
-      - master
     paths:
       - .github/workflows/ansible.molecule.fo.yml
       - infrastructure/ansible/roles/molecule_shared/**
diff --git a/.github/workflows/ansible.molecule.todb.yml b/.github/workflows/ansible.molecule.todb.yml
index 13807a140..0fc529c1c 100644
--- a/.github/workflows/ansible.molecule.todb.yml
+++ b/.github/workflows/ansible.molecule.todb.yml
@@ -20,8 +20,6 @@ name: Molecule - TODB
 on:
   create:
   push:
-    branches:
-      - master
     paths:
       - .github/workflows/ansible.molecule.todb.yml
       - infrastructure/ansible/roles/molecule_shared/**
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 768c04969..e8e85c9a5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,11 +17,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Traffic Monitor: Add support for `access.log` to TM.
 - Added functionality for login to provide a Bearer token and for that token to be later used for authorization.
 - [Traffic Ops] Added support for backend configurations so that Traffic Ops can act as a reverse proxy for these services [#6754](https://github.com/apache/trafficcontrol/pull/6754).
-- Added functionality for CDN locks, so that they can be shared amongst a list of specified usernames. 
+- Added functionality for CDN locks, so that they can be shared amongst a list of specified usernames.
 - [Traffic Ops | Traffic Go Clients | T3C] Add additional timestamp fields to server for queuing and dequeueing config and revalidate updates.
 - Added layered profile feature to 4.0 for `GET` /servers/, `POST` /servers/, `PUT` /servers/{id} and `DELETE` /servers/{id}.
 - Added a Traffic Ops endpoint and Traffic Portal page to view all CDNi configuration update requests and approve or deny.
-- Added layered profile feature to 4.0 for `GET` /servers/details.
 - Added layered profile feature to 4.0 for `GET` /deliveryservices/{id}/servers/ and /deliveryservices/{id}/servers/eligible.
 
 ### Fixed
@@ -57,6 +56,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Replace `forever` with `pm2` for process management of the traffic portal node server to remediate security issues.
 - Removed the Traffic Monitor `peer_polling_protocol` option. Traffic Monitor now just uses hostnames to request peer states, which can be handled via IPv4 or IPv6 depending on the underlying IP version in use.
 - Dropped CentOS 8 support
+- The `/servers/details` endpoint of the Traffic Ops API has been dropped in version 4.0, and marked deprecated in earlier versions.
 
 ### Changed
 - [#6654](https://github.com/apache/trafficcontrol/issues/6654) Traffic Monitor now uses the TO API 4.0 by default and falls back to 3.1
diff --git a/docs/source/api/v3/servers_details.rst b/docs/source/api/v3/servers_details.rst
index 4dfe2e96e..f1e746aa4 100644
--- a/docs/source/api/v3/servers_details.rst
+++ b/docs/source/api/v3/servers_details.rst
@@ -20,6 +20,9 @@
 *******************
 Retrieves details of :ref:`tp-configure-servers`.
 
+.. deprecated:: 3.1
+	This endpoint has been removed from the latest version of the API, and clients are advised to use :ref:`to-api-v3-servers` instead.
+
 
 ``GET``
 =======
diff --git a/docs/source/api/v4/servers_details.rst b/docs/source/api/v4/servers_details.rst
deleted file mode 100644
index dc0a917b5..000000000
--- a/docs/source/api/v4/servers_details.rst
+++ /dev/null
@@ -1,192 +0,0 @@
-..
-..
-.. Licensed 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.
-..
-
-.. _to-api-servers-details:
-
-*******************
-``servers/details``
-*******************
-Retrieves details of :ref:`tp-configure-servers`.
-
-
-``GET``
-=======
-:Auth. Required: Yes
-:Roles Required: None
-:Permissions Required: SERVER:READ, DELIVERY-SERVICE:READ, CDN:READ, PHYSICAL-LOCATION:READ, CACHE-GROUP:READ, TYPE:READ, PROFILE:READ
-:Response Type:  Array
-
-.. note:: On top of the response including the response key that is of type array it will also include the keys ``limit``, ``orderby``, and ``size``.
-
-Request Structure
------------------
-.. table:: Request Query Parameters
-
-	+----------------+----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| Name           | Required                               | Description                                                                                                                                                    |
-	+================+========================================+================================================================================================================================================================+
-	| hostName       | Required if no physLocationID provided | Return only the servers with this (short) hostname. Capitalization of "hostName" is important.                                                                 |
-	+----------------+----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| physLocationID | Required if no hostName provided       | Return only servers with this integral, unique identifier for the physical location where the server resides. Capitalization of "physLocationID" is important. |
-	+----------------+----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
-
-.. code-block:: http
-	:caption: Request Example
-
-	GET /api/4.0/servers/details?hostName=edge HTTP/1.1
-	User-Agent: python-requests/2.22.0
-	Accept-Encoding: gzip, deflate
-	Accept: */*
-	Connection: keep-alive
-	Cookie: mojolicious=...
-
-Response Structure
-------------------
-:limit:         The maximum size of the ``response`` array, also indicative of the number of results per page using the pagination requested by the query parameters (if any) - this should be the same as the ``limit`` query parameter (if given)
-:orderby:       A string that names the field by which the elements of the ``response`` array are ordered - should be the same as the ``orderby`` request query parameter (if given)
-:response:      An array of objects, each of which represents the details of a given :ref:`Server <tp-configure-servers>`.
-
-	:cachegroup:            A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
-	:cdnName:               Name of the CDN to which the server belongs
-	:deliveryservices:      An array of integral, unique identifiers for :term:`Delivery Services` to which this server belongs
-	:domainName:            The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
-	:guid:                  An identifier used to uniquely identify the server
-
-		.. note::       This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
-
-	:hostName:              The (short) hostname of the server
-	:httpsPort:             The port on which the server listens for incoming HTTPS connections/requests
-	:id:                    An integral, unique identifier for this server
-	:iloIpAddress:          The IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-	:iloIpGateway:          The IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-	:iloIpNetmask:          The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-	:iloPassword:           The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [1]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :term:`Role(s) <Role>`
-	:iloUsername:           The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-	:interfaces:     An array of interface and IP address information
-
-		:max_bandwidth:        The maximum allowed bandwidth for this interface to be considered "healthy" by Traffic Monitor. This has no effect if `monitor` is not true. Values are in kb/s. The value `0` means "no limit".
-		:monitor:              A boolean indicating if Traffic Monitor should monitor this interface
-		:mtu:                  The :abbr:`MTU (Maximum Transmission Unit)` to configure for ``interfaceName``
-
-			.. seealso:: `The Wikipedia article on Maximum Transmission Unit <https://en.wikipedia.org/wiki/Maximum_transmission_unit>`_
-
-		:name:                      The network interface name used by the server.
-
-		:ipAddresses:          An array of the IP address information for the interface
-
-			:address:          The IPv4 or IPv6 address and subnet mask of the server - applicable for the interface ``name``
-			:gateway:          The IPv4 or IPv6 gateway address of the server - applicable for the interface ``name``
-			:service_address:  A boolean determining if content will be routed to the IP address
-
-		:routerHostName:       The human-readable name of the router responsible for reaching this server's interface.
-		:routerPortName:       The human-readable name of the port used by the router responsible for reaching this server's interface.
-
-	:mgmtIpAddress:  The IPv4 address of the server's management port
-	:mgmtIpGateway:  The IPv4 gateway of the server's management port
-	:mgmtIpNetmask:  The IPv4 subnet mask of the server's management port
-	:offlineReason:         A user-entered reason why the server is in ADMIN_DOWN or OFFLINE status
-	:physLocation:          The name of the physical location where the server resides
-	:profileNames:          List of :ref:`profile-name` of the :term:`Profiles` used by this server
-	:rack:  A string indicating "server rack" location
-	:status:                The status of the server
-
-		.. seealso::    :ref:`health-proto`
-
-	:tcpPort: The port on which this server listens for incoming TCP connections
-
-		.. note::       This is typically thought of as synonymous with "HTTP port", as the port specified by ``httpsPort`` may also be used for incoming TCP connections.
-
-	:type:                  The name of the 'type' of this server
-	:xmppId:                A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
-	:xmppPasswd:            The password used in XMPP communications with the server
-
-:size:          The page number - if pagination was requested in the query parameters, else ``0`` to indicate no pagination - of the results represented by the ``response`` array. This is named "size" for legacy reasons
-
-.. 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=Mon, 24 Feb 2020 01:27:36 GMT; Max-Age=3600; HttpOnly
-	Whole-Content-Sha512: HW2F3CEpohNAvNlEDhUfXmtwpEka4dwUWFVUSSjW98aXiv10vI6ysRIcC2P9huabCz5fdHqY3tp0LR4ekwEHqw==
-	X-Server-Name: traffic_ops_golang/
-	Date: Mon, 24 Feb 2020 00:27:36 GMT
-	Content-Length: 493
-
-	{
-		"limit": 1000,
-		"orderby": "hostName",
-		"response": [
-			{
-				"cachegroup": "CDN_in_a_Box_Edge",
-				"cdnName": "CDN-in-a-Box",
-				"deliveryservices": [
-					1
-				],
-				"domainName": "infra.ciab.test",
-				"guid": null,
-				"hardwareInfo": null,
-				"hostName": "edge",
-				"httpsPort": 443,
-				"id": 5,
-				"iloIpAddress": "",
-				"iloIpGateway": "",
-				"iloIpNetmask": "",
-				"iloPassword": "",
-				"iloUsername": "",
-				"mgmtIpAddress": "",
-				"mgmtIpGateway": "",
-				"mgmtIpNetmask": "",
-				"offlineReason": "",
-				"physLocation": "Apachecon North America 2018",
-				"profileNames": ["ATS_EDGE_TIER_CACHE"],
-				"rack": "",
-				"status": "REPORTED",
-				"tcpPort": 80,
-				"type": "EDGE",
-				"xmppId": "edge",
-				"xmppPasswd": "",
-				"interfaces": [
-					{ "ipAddresses": [
-							{
-								"address": "172.16.239.100",
-								"gateway": "172.16.239.1",
-								"service_address": true
-							},
-							{
-								"address": "fc01:9400:1000:8::100",
-								"gateway": "fc01:9400:1000:8::1",
-								"service_address": true
-							}
-						],
-						"max_bandwidth": 0,
-						"monitor": true,
-						"mtu": 1500,
-						"name": "eth0",
-						"routerHostName": "",
-						"routerPortName": ""
-					}
-				]
-			}
-		],
-		"size": 1
-	}
-
-.. [1] For more information see the `Wikipedia page on Lights-Out management <https://en.wikipedia.org/wiki/Out-of-band_management>`_\ .
diff --git a/lib/go-atscfg/strategiesdotconfig.go b/lib/go-atscfg/strategiesdotconfig.go
index bd529a03c..0b2d46168 100644
--- a/lib/go-atscfg/strategiesdotconfig.go
+++ b/lib/go-atscfg/strategiesdotconfig.go
@@ -252,7 +252,7 @@ func getStrategyGroupsSection(pa *ParentAbstraction) string {
 	if len(pa.Peers) != 0 {
 		txt += "\n" + `  - &peers_group`
 		for i, peer := range pa.Peers {
-			txt += "\n" + `    - << *peer` + strconv.Itoa(i+1)
+			txt += "\n" + `    - <<: *peer` + strconv.Itoa(i+1)
 			txt += "\n" + `      weight: ` + strconv.FormatFloat(peer.Weight, 'f', 3, 64)
 		}
 	}
diff --git a/lib/go-tc/constants.go b/lib/go-tc/constants.go
index 3ccfb6696..188d604be 100644
--- a/lib/go-tc/constants.go
+++ b/lib/go-tc/constants.go
@@ -34,11 +34,6 @@ type ErrorConstant string
 // Error converts ErrorConstants to a string.
 func (e ErrorConstant) Error() string { return string(e) }
 
-// DBError is an error message for database errors.
-//
-// Deprecated: Since internal errors are not returned to users, there's no reason not to include more detail in an error message than this.
-const DBError = ErrorConstant("database access error")
-
 // NilTenantError can used when a Tenantable object finds that TentantID in the
 // request is nil.
 const NilTenantError = ErrorConstant("tenancy is enabled but request tenantID is nil")
diff --git a/lib/go-tc/steeringtarget.go b/lib/go-tc/steeringtarget.go
index 8dc13676b..dbc836225 100644
--- a/lib/go-tc/steeringtarget.go
+++ b/lib/go-tc/steeringtarget.go
@@ -58,7 +58,7 @@ type SteeringTargetNullable struct {
 // Validate implements the
 // github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api.ParseValidator
 // interface.
-func (st SteeringTargetNullable) Validate(tx *sql.Tx) error {
+func (st SteeringTargetNullable) Validate(tx *sql.Tx) (error, error) {
 	errs := []string{}
 	if st.TypeID == nil {
 		errs = append(errs, "missing typeId")
@@ -71,9 +71,9 @@ func (st SteeringTargetNullable) Validate(tx *sql.Tx) error {
 		errs = append(errs, "missing value")
 	}
 	if len(errs) > 0 {
-		return errors.New(strings.Join(errs, "; "))
+		return errors.New(strings.Join(errs, "; ")), nil
 	}
-	return nil
+	return nil, nil
 }
 
 // SteeringTargetsResponse is the type of a response from Traffic Ops to its
diff --git a/traffic_control/clients/python/trafficops/tosession.py b/traffic_control/clients/python/trafficops/tosession.py
index f907911f0..150b51311 100644
--- a/traffic_control/clients/python/trafficops/tosession.py
+++ b/traffic_control/clients/python/trafficops/tosession.py
@@ -1648,13 +1648,16 @@ class TOSession(RestApiSession):
 		:raises: Union[LoginError, OperationError]
 		"""
 
-	@api_request('get', 'servers/details?hostName={name}', ('3.0','4.0',))
+	@api_request('get', 'servers/details?hostName={name}', ('3.0',))
 	def get_server_details(self, name=None):
 		"""
 		Get servers/details
-		:ref:`to-api-servers-details`
+		:ref:`to-api-v3-servers-details`
 		:rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], requests.Response]
 		:raises: Union[LoginError, OperationError]
+
+		.. deprecated:: 3.0
+			The endpoint this represents has been removed from APIv4 and clients should use get_servers instead.
 		"""
 
 	@api_request('post', 'servercheck', ('3.0',))
diff --git a/traffic_ops/traffic_ops_golang/api/shared_handlers.go b/traffic_ops/traffic_ops_golang/api/shared_handlers.go
index a657b27f2..2e86dff6e 100644
--- a/traffic_ops/traffic_ops_golang/api/shared_handlers.go
+++ b/traffic_ops/traffic_ops_golang/api/shared_handlers.go
@@ -103,11 +103,11 @@ func GetCombinedParams(r *http.Request) (map[string]string, error) {
 }
 
 // decodeAndValidateRequestBody decodes and validates a pointer to a struct implementing the Validator interface
-func decodeAndValidateRequestBody(r *http.Request, v Validator) error {
+func decodeAndValidateRequestBody(r *http.Request, v Validator) (error, error) {
 	defer r.Body.Close()
 
 	if err := json.NewDecoder(r.Body).Decode(v); err != nil {
-		return err
+		return err, nil
 	}
 	return v.Validate()
 }
@@ -242,8 +242,12 @@ func UpdateHandler(updater Updater) http.HandlerFunc {
 		obj := reflect.New(objectType).Interface().(Updater)
 		obj.SetInfo(inf)
 
-		if err := decodeAndValidateRequestBody(r, obj); err != nil {
-			HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
+		if userErr, sysErr := decodeAndValidateRequestBody(r, obj); userErr != nil || sysErr != nil {
+			code := http.StatusBadRequest
+			if sysErr != nil {
+				code = http.StatusInternalServerError
+			}
+			HandleErr(w, r, inf.Tx.Tx, code, userErr, sysErr)
 			return
 		}
 
@@ -298,7 +302,7 @@ func UpdateHandler(updater Updater) http.HandlerFunc {
 		}
 
 		if err := CreateChangeLog(ApiChange, Updated, obj, inf.User, inf.Tx.Tx); err != nil {
-			HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, tc.DBError, errors.New("inserting changelog: "+err.Error()))
+			HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting changelog: %w", err))
 			return
 		}
 		alerts := tc.CreateAlerts(tc.SuccessLevel, obj.GetType()+" was updated.")
@@ -489,9 +493,13 @@ func CreateHandler(creator Creator) http.HandlerFunc {
 			for _, objElemInt := range objSlice {
 				objElem := reflect.ValueOf(objElemInt).Interface().(Creator)
 
-				err = objElem.Validate()
-				if err != nil {
-					HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
+				userErr, sysErr = objElem.Validate()
+				if userErr != nil || sysErr != nil {
+					code := http.StatusBadRequest
+					if sysErr != nil {
+						code = http.StatusInternalServerError
+					}
+					HandleErr(w, r, inf.Tx.Tx, code, userErr, sysErr)
 					return
 				}
 
@@ -514,7 +522,7 @@ func CreateHandler(creator Creator) http.HandlerFunc {
 				}
 
 				if err = CreateChangeLog(ApiChange, Created, objElem, inf.User, inf.Tx.Tx); err != nil {
-					HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, tc.DBError, errors.New("inserting changelog: "+err.Error()))
+					HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting changelog: %w", err))
 					return
 				}
 			}
@@ -541,9 +549,13 @@ func CreateHandler(creator Creator) http.HandlerFunc {
 			WriteAlertsObj(w, r, http.StatusOK, alerts, responseObj)
 
 		} else {
-			err := decodeAndValidateRequestBody(r, obj)
-			if err != nil {
-				HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
+			userErr, sysErr := decodeAndValidateRequestBody(r, obj)
+			if userErr != nil || sysErr != nil {
+				code := http.StatusBadRequest
+				if sysErr != nil {
+					code = http.StatusInternalServerError
+				}
+				HandleErr(w, r, inf.Tx.Tx, code, userErr, sysErr)
 				return
 			}
 
@@ -565,8 +577,8 @@ func CreateHandler(creator Creator) http.HandlerFunc {
 				return
 			}
 
-			if err = CreateChangeLog(ApiChange, Created, obj, inf.User, inf.Tx.Tx); err != nil {
-				HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, tc.DBError, errors.New("inserting changelog: "+err.Error()))
+			if err := CreateChangeLog(ApiChange, Created, obj, inf.User, inf.Tx.Tx); err != nil {
+				HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting changelog: %w", err))
 				return
 			}
 			alerts := tc.CreateAlerts(tc.SuccessLevel, obj.GetType()+" was created.")
diff --git a/traffic_ops/traffic_ops_golang/api/shared_interfaces.go b/traffic_ops/traffic_ops_golang/api/shared_interfaces.go
index d9e46fab9..1d9277393 100644
--- a/traffic_ops/traffic_ops_golang/api/shared_interfaces.go
+++ b/traffic_ops/traffic_ops_golang/api/shared_interfaces.go
@@ -20,11 +20,12 @@ package api
  */
 
 import (
+	"net/http"
+	"time"
+
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
-	"net/http"
-	"time"
 )
 
 type CRUDer interface {
@@ -103,8 +104,10 @@ type OptionsDeleter interface {
 	DeleteKeyOptions() map[string]dbhelpers.WhereColumnInfo
 }
 
+// Validator objects return user and system errors based on validation rules
+// defined by that object.
 type Validator interface {
-	Validate() error
+	Validate() (error, error)
 }
 
 type Tenantable interface {
diff --git a/traffic_ops/traffic_ops_golang/apitenant/tenant.go b/traffic_ops/traffic_ops_golang/apitenant/tenant.go
index 49c1d7558..929656490 100644
--- a/traffic_ops/traffic_ops_golang/apitenant/tenant.go
+++ b/traffic_ops/traffic_ops_golang/apitenant/tenant.go
@@ -121,15 +121,15 @@ func (ten *TOTenant) SetKeys(keys map[string]interface{}) {
 	ten.ID = &i
 }
 
-// Validate fulfills the api.Validator interface
-func (ten TOTenant) Validate() error {
+// Validate fulfills the api.Validator interface.
+func (ten TOTenant) Validate() (error, error) {
 	errs := validation.Errors{
 		"name":       validation.Validate(ten.Name, validation.Required),
 		"active":     validation.Validate(ten.Active), // only validate it's boolean
 		"parentId":   validation.Validate(ten.ParentID, validation.Required, validation.Min(1)),
 		"parentName": nil,
 	}
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (ten *TOTenant) Create() (error, error, int) { return api.GenericCreate(ten) }
diff --git a/traffic_ops/traffic_ops_golang/asn/asns.go b/traffic_ops/traffic_ops_golang/asn/asns.go
index 3e9611732..2c7ec6a87 100644
--- a/traffic_ops/traffic_ops_golang/asn/asns.go
+++ b/traffic_ops/traffic_ops_golang/asn/asns.go
@@ -95,12 +95,12 @@ func (asn TOASNV11) GetType() string {
 	return "asn"
 }
 
-func (asn TOASNV11) Validate() error {
+func (asn TOASNV11) Validate() (error, error) {
 	errs := validation.Errors{
 		"asn":          validation.Validate(asn.ASN, validation.NotNil, validation.Min(0)),
 		"cachegroupId": validation.Validate(asn.CachegroupID, validation.NotNil, validation.Min(0)),
 	}
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (as *TOASNV11) Create() (error, error, int) {
diff --git a/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go b/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go
index 155934635..46355e8fc 100644
--- a/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go
+++ b/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go
@@ -247,28 +247,31 @@ func (cg *TOCacheGroup) ValidateTypeInTopology() error {
 	return fmt.Errorf("cannot change type of cachegroup %s from %s to %s because it is in use by a topology", *cg.Name, typeNameByID[previousTypeID], typeNameByID[*cg.TypeID])
 }
 
-// Validate fulfills the api.Validator interface
-func (cg TOCacheGroup) Validate() error {
+// Validate fulfills the api.Validator interface.
+//
+// TODO: A lot of database operations here either swallow their errors or return
+// them to the client.
+func (cg TOCacheGroup) Validate() (error, error) {
 	if _, err := tc.ValidateTypeID(cg.ReqInfo.Tx.Tx, cg.TypeID, "cachegroup"); err != nil {
-		return err
+		return err, nil
 	}
 
 	if cg.Fallbacks != nil && len(*cg.Fallbacks) > 0 {
 		isValid, err := cg.isAllowedToFallback(*cg.TypeID)
 		if err != nil {
-			return err
+			return err, nil
 		}
 		if !isValid {
-			return errors.New("the cache group " + *cg.Name + " is not allowed to have fallbacks.  It must be of type EDGE_LOC.")
+			return errors.New("the cache group " + *cg.Name + " is not allowed to have fallbacks. It must be of type EDGE_LOC."), nil
 		}
 
 		for _, fallback := range *cg.Fallbacks {
 			isValid, err = cg.isValidCacheGroupFallback(fallback)
 			if err != nil {
-				return err
+				return err, nil
 			}
 			if !isValid {
-				return errors.New("the cache group " + fallback + " is not valid as a fallback.  It must exist as a cache group and be of type EDGE_LOC.")
+				return errors.New("the cache group " + fallback + " is not valid as a fallback. It must exist as a cache group and be of type EDGE_LOC."), nil
 			}
 		}
 	}
@@ -287,7 +290,7 @@ func (cg TOCacheGroup) Validate() error {
 		"localizationMethods":         validation.Validate(cg.LocalizationMethods, validation.By(tovalidate.IsPtrToSliceOfUniqueStringersICase("CZ", "DEEP_CZ", "GEO"))),
 		"type":                        cg.ValidateTypeInTopology(),
 	}
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 //The TOCacheGroup implementation of the Creator interface
@@ -393,10 +396,10 @@ func (cg *TOCacheGroup) createCacheGroupFallbacks() error {
 func (cg *TOCacheGroup) isValidCacheGroupFallback(fallbackName string) (bool, error) {
 	var isValid bool
 	query := `SELECT(
-SELECT cachegroup.id 
-FROM cachegroup 
-JOIN type on type.id = cachegroup.type 
-WHERE cachegroup.name = $1 
+SELECT cachegroup.id
+FROM cachegroup
+JOIN type on type.id = cachegroup.type
+WHERE cachegroup.name = $1
 AND (type.name = 'EDGE_LOC')
 ) IS NOT NULL;`
 
@@ -411,9 +414,9 @@ AND (type.name = 'EDGE_LOC')
 func (cg *TOCacheGroup) isAllowedToFallback(cacheGroupType int) (bool, error) {
 	var isValid bool
 	query := `SELECT(
-SELECT type.name 
-FROM type 
-WHERE type.id = $1 
+SELECT type.name
+FROM type
+WHERE type.id = $1
 AND (type.name = 'EDGE_LOC')
 ) IS NOT NULL;`
 
diff --git a/traffic_ops/traffic_ops_golang/cdn/cdns.go b/traffic_ops/traffic_ops_golang/cdn/cdns.go
index 79ccdd01a..e98a4b92a 100644
--- a/traffic_ops/traffic_ops_golang/cdn/cdns.go
+++ b/traffic_ops/traffic_ops_golang/cdn/cdns.go
@@ -120,15 +120,15 @@ func IsValidCDNName(str string) bool {
 	return i == -1
 }
 
-// Validate fulfills the api.Validator interface
-func (cdn TOCDN) Validate() error {
+// Validate fulfills the api.Validator interface.
+func (cdn TOCDN) Validate() (error, error) {
 	validName := validation.NewStringRule(IsValidCDNName, "invalid characters found - Use alphanumeric . or - .")
 	validDomainName := validation.NewStringRule(govalidator.IsDNSName, "not a valid domain name")
 	errs := validation.Errors{
 		"name":       validation.Validate(cdn.Name, validation.Required, validName),
 		"domainName": validation.Validate(cdn.DomainName, validation.Required, validDomainName),
 	}
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (cdn *TOCDN) Create() (error, error, int) {
diff --git a/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations.go b/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations.go
index 54a218e29..b15b61283 100644
--- a/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations.go
+++ b/traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations.go
@@ -35,7 +35,7 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
 	"github.com/asaskevich/govalidator"
-	"github.com/go-ozzo/ozzo-validation"
+	validation "github.com/go-ozzo/ozzo-validation"
 )
 
 // we need a type alias to define functions on
@@ -53,9 +53,9 @@ func (v *TOCDNFederation) SetLastUpdated(t tc.TimeNoMod) { v.LastUpdated = &t }
 func (v *TOCDNFederation) InsertQuery() string           { return insertQuery() }
 func (v *TOCDNFederation) SelectMaxLastUpdatedQuery(where, orderBy, pagination, tableName string) string {
 	return `SELECT max(t) from (
-		SELECT max(federation.last_updated) as t from federation 
-		join federation_deliveryservice fds on fds.federation = federation.id 
-		join deliveryservice ds on ds.id = fds.deliveryservice 
+		SELECT max(federation.last_updated) as t from federation
+		join federation_deliveryservice fds on fds.federation = federation.id
+		join deliveryservice ds on ds.id = fds.deliveryservice
 		join cdn c on c.id = ds.cdn_id ` + where + orderBy + pagination +
 		` UNION ALL
 		select max(last_updated) as t from last_deleted l where l.table_name='federation') as res`
@@ -112,7 +112,7 @@ func (fed *TOCDNFederation) SetKeys(keys map[string]interface{}) {
 }
 
 // Fulfills `Validate' interface
-func (fed *TOCDNFederation) Validate() error {
+func (fed *TOCDNFederation) Validate() (error, error) {
 
 	isDNSName := validation.NewStringRule(govalidator.IsDNSName, "must be a valid hostname")
 	endsWithDot := validation.NewStringRule(
@@ -125,7 +125,7 @@ func (fed *TOCDNFederation) Validate() error {
 		"cname": validation.Validate(fed.CName, validation.Required, endsWithDot, isDNSName),
 		"ttl":   validation.Validate(fed.TTL, validation.Required, validation.Min(0)),
 	}
-	return util.JoinErrs(tovalidate.ToErrors(validateErrs))
+	return util.JoinErrs(tovalidate.ToErrors(validateErrs)), nil
 }
 
 func (fed *TOCDNFederation) CheckIfCDNAndFederationMatch(cdnName string) (error, error, int) {
diff --git a/traffic_ops/traffic_ops_golang/coordinate/coordinates.go b/traffic_ops/traffic_ops_golang/coordinate/coordinates.go
index 51d19fc10..29b40589b 100644
--- a/traffic_ops/traffic_ops_golang/coordinate/coordinates.go
+++ b/traffic_ops/traffic_ops_golang/coordinate/coordinates.go
@@ -110,8 +110,8 @@ func IsValidCoordinateName(str string) bool {
 	return i == -1
 }
 
-// Validate fulfills the api.Validator interface
-func (coordinate TOCoordinate) Validate() error {
+// Validate fulfills the api.Validator interface.
+func (coordinate TOCoordinate) Validate() (error, error) {
 	validName := validation.NewStringRule(IsValidCoordinateName, "invalid characters found - Use alphanumeric . or - or _ .")
 	latitudeErr := "Must be a floating point number within the range +-90"
 	longitudeErr := "Must be a floating point number within the range +-180"
@@ -120,7 +120,7 @@ func (coordinate TOCoordinate) Validate() error {
 		"latitude":  validation.Validate(coordinate.Latitude, validation.Min(-90.0).Error(latitudeErr), validation.Max(90.0).Error(latitudeErr)),
 		"longitude": validation.Validate(coordinate.Longitude, validation.Min(-180.0).Error(longitudeErr), validation.Max(180.0).Error(longitudeErr)),
 	}
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (coord *TOCoordinate) Create() (error, error, int) { return api.GenericCreate(coord) }
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index b7157716f..feaf30c07 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -2124,10 +2124,17 @@ func UpdateServerProfilesForV4(id int, profile []string, tx *sql.Tx) error {
 }
 
 // UpdateServerProfileTableForV2V3 updates CommonServerPropertiesV40 struct and server_profile table via Update (server) function for API v2/v3.
-func UpdateServerProfileTableForV2V3(id *int, newProfile *string, origProfile string, tx *sql.Tx) ([]string, error) {
+func UpdateServerProfileTableForV2V3(id *int, newProfileId *int, origProfile string, tx *sql.Tx) ([]string, error) {
 	var profileName []string
+
+	var newProfile string
+	err := tx.QueryRow("SELECT name from profile where id = $1", *newProfileId).Scan(&newProfile)
+	if err != nil && err != sql.ErrNoRows {
+		return nil, fmt.Errorf("selecting profile by name: %w", err)
+	}
+
 	query := `UPDATE server_profile SET profile_name=$1 WHERE server=$2 AND profile_name=$3`
-	_, err := tx.Exec(query, *newProfile, *id, origProfile)
+	_, err = tx.Exec(query, newProfile, *id, origProfile)
 	if err != nil {
 		return nil, fmt.Errorf("updating server_profile by profile_name: %w", err)
 	}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go
index 5f045c56d..3966186ee 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go
@@ -1284,8 +1284,7 @@ func readGetDeliveryServices(h http.Header, params map[string]string, tx *sqlx.T
 		accessibleTo, _ := strconv.Atoi(accessibleTo)
 		accessibleTenants, err := tenant.GetUserTenantIDListTx(tx.Tx, accessibleTo)
 		if err != nil {
-			log.Errorln("unable to get tenants: " + err.Error())
-			return nil, nil, tc.DBError, http.StatusInternalServerError, &maxTime
+			return nil, nil, fmt.Errorf("unable to get tenants: %w", err), http.StatusInternalServerError, &maxTime
 		}
 		where += " AND ds.tenant_id = ANY(CAST(:accessibleTo AS bigint[])) "
 		queryValues["accessibleTo"] = pq.Array(accessibleTenants)
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_required_capabilities.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_required_capabilities.go
index 043495132..b6b75b3dd 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_required_capabilities.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_required_capabilities.go
@@ -155,13 +155,13 @@ func (rc *RequiredCapability) GetType() string {
 }
 
 // Validate implements the api.Validator interface.
-func (rc RequiredCapability) Validate() error {
+func (rc RequiredCapability) Validate() (error, error) {
 	errs := validation.Errors{
 		deliveryServiceQueryParam:    validation.Validate(rc.DeliveryServiceID, validation.Required),
 		requiredCapabilityQueryParam: validation.Validate(rc.RequiredCapability, validation.Required),
 	}
 
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 // Update implements the api.CRUDer interface.
@@ -344,8 +344,8 @@ func (rc *RequiredCapability) checkServerCap() (error, error, int) {
 	// Get server capability name
 	name := ""
 	if err := tx.QueryRow(`
-		SELECT name 
-		FROM server_capability 
+		SELECT name
+		FROM server_capability
 		WHERE name = $1`, rc.RequiredCapability).Scan(&name); err != nil && err != sql.ErrNoRows {
 		return nil, fmt.Errorf("querying server capability for name '%v': %v", rc.RequiredCapability, err), http.StatusInternalServerError
 	}
@@ -436,7 +436,7 @@ func (rc *RequiredCapability) ensureDSServerCap() (error, error, int) {
 	dsServerIDs := []int64{}
 	if err := tx.Tx.QueryRow(`
 	SELECT ARRAY(
-		SELECT ds.server 
+		SELECT ds.server
 		FROM deliveryservice_server ds
 		JOIN server s ON ds.server = s.id
 		JOIN type t ON s.type = t.id
@@ -455,7 +455,7 @@ func (rc *RequiredCapability) ensureDSServerCap() (error, error, int) {
 	if err := tx.QueryRow(`
 	SELECT ARRAY(
 		SELECT server
-		FROM server_server_capability 
+		FROM server_server_capability
 		WHERE server = ANY($1)
 		AND server_capability=$2
 	)`, pq.Array(dsServerIDs), rc.RequiredCapability).Scan(pq.Array(&capServerIDs)); err != nil && err != sql.ErrNoRows {
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments.go
index b58a027f7..3c08d1c92 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments.go
@@ -31,7 +31,7 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
 
-	"github.com/go-ozzo/ozzo-validation"
+	validation "github.com/go-ozzo/ozzo-validation"
 )
 
 //we need a type alias to define functions on
@@ -98,12 +98,12 @@ func (comment TODeliveryServiceRequestComment) GetType() string {
 	return "deliveryservice_request_comment"
 }
 
-func (comment TODeliveryServiceRequestComment) Validate() error {
+func (comment TODeliveryServiceRequestComment) Validate() (error, error) {
 	errs := validation.Errors{
 		"deliveryServiceRequestId": validation.Validate(comment.DeliveryServiceRequestID, validation.NotNil),
 		"value":                    validation.Validate(comment.Value, validation.NotNil),
 	}
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (comment *TODeliveryServiceRequestComment) Create() (error, error, int) {
diff --git a/traffic_ops/traffic_ops_golang/division/divisions.go b/traffic_ops/traffic_ops_golang/division/divisions.go
index 7bcf4b03f..57d45cc92 100644
--- a/traffic_ops/traffic_ops_golang/division/divisions.go
+++ b/traffic_ops/traffic_ops_golang/division/divisions.go
@@ -95,11 +95,11 @@ func (division TODivision) GetType() string {
 	return "division"
 }
 
-func (division TODivision) Validate() error {
+func (division TODivision) Validate() (error, error) {
 	errs := validation.Errors{
 		"name": validation.Validate(division.Name, validation.NotNil, validation.Required),
 	}
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (dv *TODivision) Create() (error, error, int) { return api.GenericCreate(dv) }
diff --git a/traffic_ops/traffic_ops_golang/origin/origins.go b/traffic_ops/traffic_ops_golang/origin/origins.go
index bfb152376..de94740dc 100644
--- a/traffic_ops/traffic_ops_golang/origin/origins.go
+++ b/traffic_ops/traffic_ops_golang/origin/origins.go
@@ -84,7 +84,7 @@ func (origin *TOOrigin) GetType() string {
 	return "origin"
 }
 
-func (origin *TOOrigin) Validate() error {
+func (origin *TOOrigin) Validate() (error, error) {
 
 	noSpaces := validation.NewStringRule(tovalidate.NoSpaces, "cannot contain spaces")
 	validProtocol := validation.NewStringRule(tovalidate.IsOneOfStringICase("http", "https"), "must be http or https")
@@ -103,7 +103,7 @@ func (origin *TOOrigin) Validate() error {
 		"protocol":          validation.Validate(origin.Protocol, validation.Required, validProtocol),
 		"tenantId":          validation.Validate(origin.TenantID, validation.Min(1)),
 	}
-	return util.JoinErrs(tovalidate.ToErrors(validateErrs))
+	return util.JoinErrs(tovalidate.ToErrors(validateErrs)), nil
 }
 
 // GetTenantID returns a pointer to the Origin's tenant ID from the Tx, whether or not the Origin exists, and any error encountered
@@ -182,8 +182,7 @@ func getOrigins(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth
 
 	tenantIDs, err := tenant.GetUserTenantIDListTx(tx.Tx, user.TenantID)
 	if err != nil {
-		log.Errorln("received error querying for user's tenants: " + err.Error())
-		return nil, nil, tc.DBError, http.StatusInternalServerError, nil
+		return nil, nil, fmt.Errorf("received error querying for user's tenants: %w", err), http.StatusInternalServerError, nil
 	}
 	where, queryValues = dbhelpers.AddTenancyCheck(where, queryValues, "o.tenant", tenantIDs)
 
diff --git a/traffic_ops/traffic_ops_golang/parameter/parameters.go b/traffic_ops/traffic_ops_golang/parameter/parameters.go
index d1ccc4e3e..8d719fa3d 100644
--- a/traffic_ops/traffic_ops_golang/parameter/parameters.go
+++ b/traffic_ops/traffic_ops_golang/parameter/parameters.go
@@ -119,7 +119,7 @@ func (param *TOParameter) GetType() string {
 }
 
 // Validate fulfills the api.Validator interface
-func (param TOParameter) Validate() error {
+func (param TOParameter) Validate() (error, error) {
 	// Test
 	// - Secure Flag is always set to either 1/0
 	// - Admin rights only
@@ -133,7 +133,7 @@ func (param TOParameter) Validate() error {
 		errs[atscfg.ParentConfigFileName+" "+atscfg.ParentConfigCacheParamWeight] = validation.Validate(*param.Value, tovalidate.StringIsValidFloat())
 	}
 
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (pa *TOParameter) Create() (error, error, int) {
diff --git a/traffic_ops/traffic_ops_golang/physlocation/phys_locations.go b/traffic_ops/traffic_ops_golang/physlocation/phys_locations.go
index d6fbd8590..51b31c455 100644
--- a/traffic_ops/traffic_ops_golang/physlocation/phys_locations.go
+++ b/traffic_ops/traffic_ops_golang/physlocation/phys_locations.go
@@ -90,7 +90,7 @@ func (pl *TOPhysLocation) GetType() string {
 	return "physLocation"
 }
 
-func (pl *TOPhysLocation) Validate() error {
+func (pl *TOPhysLocation) Validate() (error, error) {
 	errs := validation.Errors{
 		"address":   validation.Validate(pl.Address, validation.Required),
 		"city":      validation.Validate(pl.City, validation.Required),
@@ -101,9 +101,9 @@ func (pl *TOPhysLocation) Validate() error {
 		"zip":       validation.Validate(pl.Zip, validation.Required),
 	}
 	if errs != nil {
-		return util.JoinErrs(tovalidate.ToErrors(errs))
+		return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 	}
-	return nil
+	return nil, nil
 }
 
 func (pl *TOPhysLocation) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
diff --git a/traffic_ops/traffic_ops_golang/profile/profiles.go b/traffic_ops/traffic_ops_golang/profile/profiles.go
index 4ae0e8ae4..2305e8b8a 100644
--- a/traffic_ops/traffic_ops_golang/profile/profiles.go
+++ b/traffic_ops/traffic_ops_golang/profile/profiles.go
@@ -102,7 +102,7 @@ func (prof *TOProfile) GetType() string {
 	return "profile"
 }
 
-func (prof *TOProfile) Validate() error {
+func (prof *TOProfile) Validate() (error, error) {
 	errs := validation.Errors{
 		NameQueryParam: validation.Validate(prof.Name, validation.By(
 			func(value interface{}) error {
@@ -124,9 +124,9 @@ func (prof *TOProfile) Validate() error {
 		TypeQueryParam:        validation.Validate(prof.Type, validation.Required),
 	}
 	if errs != nil {
-		return util.JoinErrs(tovalidate.ToErrors(errs))
+		return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 	}
-	return nil
+	return nil, nil
 }
 
 func (prof *TOProfile) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
diff --git a/traffic_ops/traffic_ops_golang/profileparameter/profile_parameters.go b/traffic_ops/traffic_ops_golang/profileparameter/profile_parameters.go
index 006566030..b66e4379d 100644
--- a/traffic_ops/traffic_ops_golang/profileparameter/profile_parameters.go
+++ b/traffic_ops/traffic_ops_golang/profileparameter/profile_parameters.go
@@ -98,15 +98,15 @@ func (pp *TOProfileParameter) SetKeys(keys map[string]interface{}) {
 	pp.ParameterID = &paramId
 }
 
-// Validate fulfills the api.Validator interface
-func (pp *TOProfileParameter) Validate() error {
+// Validate fulfills the api.Validator interface.
+func (pp *TOProfileParameter) Validate() (error, error) {
 
 	errs := validation.Errors{
 		"profileId":   validation.Validate(pp.ProfileID, validation.Required),
 		"parameterId": validation.Validate(pp.ParameterID, validation.Required),
 	}
 
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 //The TOProfileParameter implementation of the Creator interface
diff --git a/traffic_ops/traffic_ops_golang/region/regions.go b/traffic_ops/traffic_ops_golang/region/regions.go
index 27a180722..9263059fb 100644
--- a/traffic_ops/traffic_ops_golang/region/regions.go
+++ b/traffic_ops/traffic_ops_golang/region/regions.go
@@ -94,14 +94,14 @@ func (region *TORegion) GetType() string {
 	return "region"
 }
 
-func (region *TORegion) Validate() error {
+func (region *TORegion) Validate() (error, error) {
 	if len(region.Name) < 1 {
-		return errors.New(`region 'name' is required`)
+		return errors.New(`region 'name' is required`), nil
 	}
 	if region.Division == 0 {
-		return errors.New(`region 'division' is required`)
+		return errors.New(`region 'division' is required`), nil
 	}
-	return nil
+	return nil, nil
 }
 
 func (rg *TORegion) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
diff --git a/traffic_ops/traffic_ops_golang/role/roles.go b/traffic_ops/traffic_ops_golang/role/roles.go
index 623958eb1..43878e53b 100644
--- a/traffic_ops/traffic_ops_golang/role/roles.go
+++ b/traffic_ops/traffic_ops_golang/role/roles.go
@@ -129,7 +129,7 @@ func (role *TORole) SetKeys(keys map[string]interface{}) {
 }
 
 // Validate fulfills the api.Validator interface
-func (role TORole) Validate() error {
+func (role TORole) Validate() (error, error) {
 	errs := validation.Errors{
 		"name":        validation.Validate(role.Name, validation.Required),
 		"description": validation.Validate(role.Description, validation.Required),
@@ -141,14 +141,13 @@ func (role TORole) Validate() error {
 	if role.ReqInfo.Tx != nil {
 		err := role.ReqInfo.Tx.Select(&badCaps, checkCaps, pq.Array(role.Capabilities))
 		if err != nil {
-			log.Errorf("got error from selecting bad capabilities: %v", err)
-			return tc.DBError
+			return nil, fmt.Errorf("got error from selecting bad capabilities: %w", err)
 		}
 		if len(badCaps) > 0 {
 			errsToReturn = append(errsToReturn, fmt.Errorf("can not add non-existent capabilities: %v", badCaps))
 		}
 	}
-	return util.JoinErrs(errsToReturn)
+	return util.JoinErrs(errsToReturn), nil
 }
 
 func (role *TORole) Create() (error, error, int) {
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index cbfa25030..452ab7615 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -311,9 +311,6 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodGet, Path: `servercheck/extensions$`, Handler: extensions.Get, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVER-CHECK:READ", "SERVER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4834985993},
 		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodDelete, Path: `servercheck/extensions/{id}$`, Handler: extensions.Delete, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVER-CHECK:DELETE", "SERVER-CHECK:READ", "SERVER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4804982993},
 
-		//Server Details
-		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodGet, Path: `servers/details/?$`, Handler: server.GetDetailParamHandler, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVER:READ", "DELIVERY-SERVICE:READ", "CDN:READ", "PHYSICAL-LOCATION:READ", "CACHE-GROUP:READ", "TYPE:READ", "PROFILE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 42612647143},
-
 		//Server status
 		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPut, Path: `servers/{id}/status$`, Handler: server.UpdateStatusHandler, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ", "STATUS:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4766638513},
 		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPost, Path: `servers/{id}/queue_update$`, Handler: server.QueueUpdateHandler, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:QUEUE", "SERVER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 41894713},
diff --git a/traffic_ops/traffic_ops_golang/server/detail.go b/traffic_ops/traffic_ops_golang/server/detail.go
index 287e0e401..a327a4059 100644
--- a/traffic_ops/traffic_ops_golang/server/detail.go
+++ b/traffic_ops/traffic_ops_golang/server/detail.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
 
+	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
@@ -36,27 +37,32 @@ import (
 	"github.com/lib/pq"
 )
 
+// GetDetailParamHandler handles GET requests to /servers/details (the name
+// includes "Param" for legacy reasons).
+//
+// Deprecated: This endpoint has been removed from APIv4.
 func GetDetailParamHandler(w http.ResponseWriter, r *http.Request) {
+	alt := "/servers"
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
 	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		api.HandleDeprecatedErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr, &alt)
 		return
 	}
 	defer inf.Close()
 
 	hostName := inf.Params["hostName"]
 	physLocationIDStr := inf.Params["physLocationID"]
-	physLocationID := -1
+	var physLocationID int
 	if physLocationIDStr != "" {
-		err := error(nil)
+		var err error
 		physLocationID, err = strconv.Atoi(physLocationIDStr)
 		if err != nil {
-			api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("physLocationID parameter is not an integer"), nil)
+			api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("physLocationID parameter is not an integer"), err, &alt)
 			return
 		}
 	}
 	if hostName == "" && physLocationIDStr == "" {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("missing required fields: 'hostName' or 'physLocationID'"), nil)
+		api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("missing required fields: 'hostName' or 'physLocationID'"), nil, &alt)
 		return
 	}
 	orderBy := "hostName"
@@ -65,22 +71,22 @@ func GetDetailParamHandler(w http.ResponseWriter, r *http.Request) {
 	}
 	limit := 1000
 	if limitStr, ok := inf.Params["limit"]; ok {
-		err := error(nil)
+		var err error
 		limit, err = strconv.Atoi(limitStr)
 		if err != nil {
-			api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("limit parameter is not an integer"), nil)
+			api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("limit parameter is not an integer"), err, &alt)
 			return
 		}
 	}
 	servers, err := getDetailServers(inf.Tx.Tx, inf.User, hostName, physLocationID, util.CamelToSnakeCase(orderBy), limit, *inf.Version)
-	respVals := map[string]interface{}{
-		"orderby": orderBy,
-		"limit":   limit,
-		"size":    len(servers),
+	if err != nil {
+		api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err, &alt)
 	}
+	var resp interface{}
+	size := len(servers)
 
 	if inf.Version.Major <= 2 {
-		v11ServerList := []tc.ServerDetailV11{}
+		v11ServerList := make([]tc.ServerDetailV11, 0, size)
 		for _, server := range servers {
 			interfaces := server.ServerInterfaces
 			routerHostName := ""
@@ -93,24 +99,23 @@ func GetDetailParamHandler(w http.ResponseWriter, r *http.Request) {
 			v11server := tc.ServerDetailV11{}
 			v11server.ServerDetail, err = dbhelpers.GetServerDetailFromV4(server, inf.Tx.Tx)
 			if err != nil {
-				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("failed to GetServerDetailFromV4: %w", err))
+				api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("failed to GetServerDetailFromV4: %w", err), &alt)
 				return
 			}
 			v11server.RouterHostName = &routerHostName
 			v11server.RouterPortName = &routerPortName
 			legacyInterface, err := tc.V4InterfaceInfoToLegacyInterfaces(interfaces)
 			if err != nil {
-				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("converting to server detail v11: "+err.Error()))
+				api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("converting to server detail v11: %w", err), &alt)
 				return
 			}
 			v11server.LegacyInterfaceDetails = legacyInterface
 
 			v11ServerList = append(v11ServerList, v11server)
 		}
-		api.RespWriterVals(w, r, inf.Tx.Tx, respVals)(v11ServerList, err)
-		return
+		resp = v11ServerList
 	} else if inf.Version.Major <= 3 {
-		v3ServerList := []tc.ServerDetailV30{}
+		v3ServerList := make([]tc.ServerDetailV30, 0, size)
 		for _, server := range servers {
 			v3Server := tc.ServerDetailV30{}
 			interfaces := server.ServerInterfaces
@@ -123,33 +128,49 @@ func GetDetailParamHandler(w http.ResponseWriter, r *http.Request) {
 			}
 			v3Server.ServerDetail, err = dbhelpers.GetServerDetailFromV4(server, inf.Tx.Tx)
 			if err != nil {
-				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("failed to GetServerDetailFromV4: %w", err))
+				api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("failed to GetServerDetailFromV4: %w", err), &alt)
 				return
 			}
 			v3Server.RouterHostName = &routerHostName
 			v3Server.RouterPortName = &routerPortName
 			v3Interfaces, err := tc.V4InterfaceInfoToV3Interfaces(interfaces)
 			if err != nil {
-				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("converting to server detail v3: "+err.Error()))
+				api.HandleDeprecatedErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("converting to server detail v3: %w", err), &alt)
 				return
 			}
 			v3Server.ServerInterfaces = &v3Interfaces
 			v3ServerList = append(v3ServerList, v3Server)
 		}
-		api.RespWriterVals(w, r, inf.Tx.Tx, respVals)(v3ServerList, err)
+		resp = v3ServerList
+	} else {
+		api.WriteRespAlertNotFound(w, r)
 		return
 	}
-	api.RespWriterVals(w, r, inf.Tx.Tx, respVals)(servers, err)
+
+	api.WriteRespVals(w, r, resp, map[string]interface{}{
+		"alerts":  api.CreateDeprecationAlerts(&alt).Alerts,
+		"limit":   limit,
+		"orderby": orderBy,
+		"size":    size,
+	})
 }
 
+// AddWhereClauseAndQuery adds a WHERE clause to the query given in `q` (does
+// NOT check for existing WHERE clauses or that the end of the string is the
+// proper place to put one!) that limits the query results to those with the
+// given hostname and/or Physical Location ID and, with orderByStr and limitStr
+// appended (in that order), returns the result of querying the given
+// transaction.
+// Use an empty string for the hostname to not filter by hostname, use -1 as
+// physLocationID to not filter by Physical Location.
 func AddWhereClauseAndQuery(tx *sql.Tx, q string, hostName string, physLocationID int, orderByStr string, limitStr string) (*sql.Rows, error) {
-	if hostName != "" && physLocationID != -1 {
+	if hostName != "" && physLocationID != 0 {
 		q += ` WHERE server.host_name = $1::text AND server.phys_location = $2::bigint` + orderByStr + limitStr
 		return tx.Query(q, hostName, physLocationID)
 	} else if hostName != "" {
 		q += ` WHERE server.host_name = $1::text` + orderByStr + limitStr
 		return tx.Query(q, hostName)
-	} else if physLocationID != -1 {
+	} else if physLocationID != 0 {
 		q += ` WHERE server.phys_location = $1::int` + orderByStr + limitStr
 		return tx.Query(q, physLocationID)
 	} else {
@@ -158,6 +179,40 @@ func AddWhereClauseAndQuery(tx *sql.Tx, q string, hostName string, physLocationI
 	}
 }
 
+const dataFetchQuery = `,
+cg.name AS cachegroup,
+cdn.name AS cdn_name,
+ARRAY(select deliveryservice from deliveryservice_server where server = server.id),
+server.domain_name,
+server.guid,
+server.host_name,
+server.https_port,
+server.ilo_ip_address,
+server.ilo_ip_gateway,
+server.ilo_ip_netmask,
+server.ilo_password,
+server.ilo_username,
+(SELECT address FROM ip_address WHERE service_address = true AND family(address) = 4 AND server = server.id) AS service_ip,
+(SELECT address FROM ip_address WHERE service_address = true AND family(address) = 6 AND server = server.id) AS service_ip6,
+(SELECT gateway FROM ip_address WHERE service_address = true AND family(address) = 4 AND server = server.id) AS service_gateway,
+(SELECT gateway FROM ip_address WHERE service_address = true AND family(address) = 6 AND server = server.id) AS service_gateway6,
+(SELECT host(netmask(ip_address.address)) FROM ip_address WHERE service_address = true AND family(address) = 4 AND server = server.id) AS service_netmask,
+(SELECT interface FROM ip_address WHERE service_address = true AND family(address) = 4 AND server = server.id) AS interface_name,
+(SELECT mtu FROM interface WHERE server.id = interface.server AND interface.name = (SELECT interface FROM ip_address WHERE service_address = true AND family(address) = 4 AND server = server.id)) AS interface_mtu,
+server.mgmt_ip_address,
+server.mgmt_ip_gateway,
+server.mgmt_ip_netmask,
+server.offline_reason,
+pl.name as phys_location,
+(SELECT ARRAY_AGG(profile_name) FROM server_profile WHERE server_profile.server=server.id) AS profile_name,
+server.rack,
+st.name as status,
+server.tcp_port,
+t.name as server_type,
+server.xmpp_id,
+server.xmpp_passwd
+`
+
 func getDetailServers(tx *sql.Tx, user *auth.CurrentUser, hostName string, physLocationID int, orderBy string, limit int, reqVersion api.Version) ([]tc.ServerDetailV40, error) {
 	allowedOrderByCols := map[string]string{
 		"":                "",
@@ -192,39 +247,6 @@ func getDetailServers(tx *sql.Tx, user *auth.CurrentUser, hostName string, physL
 		return nil, errors.New("orderBy '" + orderBy + "' not permitted")
 	}
 
-	dataFetchQuery := `,
-cg.name AS cachegroup,
-cdn.name AS cdn_name,
-ARRAY(select deliveryservice from deliveryservice_server where server = server.id),
-server.domain_name,
-server.guid,
-server.host_name,
-server.https_port,
-server.ilo_ip_address,
-server.ilo_ip_gateway,
-server.ilo_ip_netmask,
-server.ilo_password,
-server.ilo_username,
-(SELECT address FROM ip_address WHERE service_address = true AND family(address) = 4 AND server = server.id) AS service_ip,
-(SELECT address FROM ip_address WHERE service_address = true AND family(address) = 6 AND server = server.id) AS service_ip6,
-(SELECT gateway FROM ip_address WHERE service_address = true AND family(address) = 4 AND server = server.id) AS service_gateway,
-(SELECT gateway FROM ip_address WHERE service_address = true AND family(address) = 6 AND server = server.id) AS service_gateway6,
-(SELECT host(netmask(ip_address.address)) FROM ip_address WHERE service_address = true AND family(address) = 4 AND server = server.id) AS service_netmask,
-(SELECT interface FROM ip_address WHERE service_address = true AND family(address) = 4 AND server = server.id) AS interface_name,
-(SELECT mtu FROM interface WHERE server.id = interface.server AND interface.name = (SELECT interface FROM ip_address WHERE service_address = true AND family(address) = 4 AND server = server.id)) AS interface_mtu,
-server.mgmt_ip_address,
-server.mgmt_ip_gateway,
-server.mgmt_ip_netmask,
-server.offline_reason,
-pl.name as phys_location,
-(SELECT ARRAY_AGG(profile_name) FROM server_profile WHERE server_profile.server=server.id) AS profile_name,
-server.rack,
-st.name as status,
-server.tcp_port,
-t.name as server_type,
-server.xmpp_id,
-server.xmpp_passwd
-`
 	queryFormatString := `
 SELECT
 	server.id
@@ -247,42 +269,43 @@ JOIN type t ON server.type = t.id
 	}
 	idRows, err := AddWhereClauseAndQuery(tx, fmt.Sprintf(queryFormatString, ""), hostName, physLocationID, orderByStr, limitStr)
 	if err != nil {
-		return nil, errors.New("querying delivery service eligible servers: " + err.Error())
+		return nil, fmt.Errorf("querying delivery service eligible servers: %w", err)
 	}
-	defer idRows.Close()
+	defer log.Close(idRows, "getting IDs for server details names")
 	var serverIDs []int
 	for idRows.Next() {
 		var serverID *int
 		err := idRows.Scan(&serverID)
 		if err != nil {
-			return nil, errors.New("querying delivery service eligible server ids: " + err.Error())
+			return nil, fmt.Errorf("querying delivery service eligible server ids: %w", err)
 		}
 		serverIDs = append(serverIDs, *serverID)
 	}
 	serversMap, err := dbhelpers.GetServersInterfaces(serverIDs, tx)
 	if err != nil {
-		return nil, errors.New("unable to get server interfaces: " + err.Error())
+		return nil, fmt.Errorf("unable to get server interfaces: %w", err)
 	}
 	rows, err := AddWhereClauseAndQuery(tx, fmt.Sprintf(queryFormatString, dataFetchQuery), hostName, physLocationID, orderByStr, limitStr)
 	if err != nil {
-		return nil, errors.New("Error querying detail servers: " + err.Error())
+		return nil, fmt.Errorf("querying detail servers: %w", err)
 	}
 
-	defer rows.Close()
+	defer log.Close(rows, "getting server details data")
 	sIDs := []int{}
 	servers := []tc.ServerDetailV40{}
 
-	serviceAddress := util.StrPtr("")
-	service6Address := util.StrPtr("")
-	serviceGateway := util.StrPtr("")
-	service6Gateway := util.StrPtr("")
-	serviceNetmask := util.StrPtr("")
-	serviceInterface := util.StrPtr("")
-	serviceMtu := util.StrPtr("")
+	serviceAddress := new(string)
+	service6Address := new(string)
+	serviceGateway := new(string)
+	service6Gateway := new(string)
+	serviceNetmask := new(string)
+	serviceInterface := new(string)
+	serviceMtu := new(string)
 
 	for rows.Next() {
 		s := tc.ServerDetailV40{}
-		if err := rows.Scan(&s.ID,
+		err = rows.Scan(
+			&s.ID,
 			&s.CacheGroup,
 			&s.CDNName,
 			pq.Array(&s.DeliveryServiceIDs),
@@ -313,8 +336,10 @@ JOIN type t ON server.type = t.id
 			&s.TCPPort,
 			&s.Type,
 			&s.XMPPID,
-			&s.XMPPPasswd); err != nil {
-			return nil, errors.New("Error scanning detail server: " + err.Error())
+			&s.XMPPPasswd,
+		)
+		if err != nil {
+			return nil, fmt.Errorf("scanning detail server: %w", err)
 		}
 		s.ServerInterfaces = []tc.ServerInterfaceInfoV40{}
 		if interfacesMap, ok := serversMap[*s.ID]; ok {
@@ -335,16 +360,16 @@ JOIN type t ON server.type = t.id
 
 	rows, err = tx.Query(`SELECT serverid, description, val from hwinfo where serverid = ANY($1);`, pq.Array(sIDs))
 	if err != nil {
-		return nil, errors.New("Error querying detail servers hardware info: " + err.Error())
+		return nil, fmt.Errorf("querying detail servers hardware info: %w", err)
 	}
-	defer rows.Close()
+	defer log.Close(rows, "getting hwinfo data")
 	hwInfos := map[int]map[string]string{}
 	for rows.Next() {
 		serverID := 0
 		desc := ""
 		val := ""
 		if err := rows.Scan(&serverID, &desc, &val); err != nil {
-			return nil, errors.New("Error scanning detail server hardware info: " + err.Error())
+			return nil, fmt.Errorf("scanning detail server hardware info: %w", err)
 		}
 
 		hwInfo, ok := hwInfos[serverID]
diff --git a/traffic_ops/traffic_ops_golang/server/servers.go b/traffic_ops/traffic_ops_golang/server/servers.go
index 5c719e7fb..205fe9807 100644
--- a/traffic_ops/traffic_ops_golang/server/servers.go
+++ b/traffic_ops/traffic_ops_golang/server/servers.go
@@ -402,7 +402,7 @@ func newUUID() *string {
 	return &uuidReference
 }
 
-func validateCommon(s *tc.CommonServerProperties, tx *sql.Tx) []error {
+func validateCommon(s *tc.CommonServerProperties, tx *sql.Tx) ([]error, error) {
 
 	noSpaces := validation.NewStringRule(tovalidate.NoSpaces, "cannot contain spaces")
 
@@ -420,7 +420,7 @@ func validateCommon(s *tc.CommonServerProperties, tx *sql.Tx) []error {
 	})
 
 	if len(errs) > 0 {
-		return errs
+		return errs, nil
 	}
 
 	if _, err := tc.ValidateTypeID(tx, s.TypeID, "server"); err != nil {
@@ -429,20 +429,18 @@ func validateCommon(s *tc.CommonServerProperties, tx *sql.Tx) []error {
 
 	var cdnID int
 	if err := tx.QueryRow("SELECT cdn from profile WHERE id=$1", s.ProfileID).Scan(&cdnID); err != nil {
-		log.Errorf("could not execute select cdnID from profile: %s\n", err)
-		if err == sql.ErrNoRows {
-			errs = append(errs, fmt.Errorf("no such profileId: '%d'", *s.ProfileID))
-		} else {
-			errs = append(errs, tc.DBError)
+		if errors.Is(err, sql.ErrNoRows) {
+			errs = append(errs, fmt.Errorf("no such Profile: #%d", *s.ProfileID))
+			return errs, nil
 		}
-		return errs
+		return nil, fmt.Errorf("could not execute select cdnID from profile: %w", err)
 	}
 
 	log.Infof("got cdn id: %d from profile and cdn id: %d from server", cdnID, *s.CDNID)
 	if cdnID != *s.CDNID {
 		errs = append(errs, fmt.Errorf("CDN id '%d' for profile '%d' does not match Server CDN '%d'", cdnID, *s.ProfileID, *s.CDNID))
 	}
-	return errs
+	return errs, nil
 }
 
 func validateCommonV40(s *tc.ServerV40, tx *sql.Tx) ([]error, error) {
@@ -495,7 +493,7 @@ func validateCommonV40(s *tc.ServerV40, tx *sql.Tx) ([]error, error) {
 	return errs, nil
 }
 
-func validateV1(s *tc.ServerNullableV11, tx *sql.Tx) error {
+func validateV1(s *tc.ServerNullableV11, tx *sql.Tx) (error, error) {
 	if s.IP6Address != nil && len(strings.TrimSpace(*s.IP6Address)) == 0 {
 		s.IP6Address = nil
 	}
@@ -520,16 +518,17 @@ func validateV1(s *tc.ServerNullableV11, tx *sql.Tx) error {
 	}
 
 	errs = append(errs, tovalidate.ToErrors(validateErrs)...)
-	errs = append(errs, validateCommon(&s.CommonServerProperties, tx)...)
+	commonErrs, sysErr := validateCommon(&s.CommonServerProperties, tx)
+	errs = append(errs, commonErrs...)
 
-	return util.JoinErrs(errs)
+	return util.JoinErrs(errs), sysErr
 }
 
-func validateV2(s *tc.ServerNullableV2, tx *sql.Tx) error {
+func validateV2(s *tc.ServerNullableV2, tx *sql.Tx) (error, error) {
 	var errs []error
 
-	if err := validateV1(&s.ServerNullableV11, tx); err != nil {
-		return err
+	if userErr, sysErr := validateV1(&s.ServerNullableV11, tx); userErr != nil || sysErr != nil {
+		return userErr, sysErr
 	}
 
 	// default boolean value is false
@@ -551,7 +550,7 @@ func validateV2(s *tc.ServerNullableV2, tx *sql.Tx) error {
 	if *s.IP6IsService && (s.IP6Address == nil || *s.IP6Address == "") {
 		errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
 	}
-	return util.JoinErrs(errs)
+	return util.JoinErrs(errs), nil
 }
 
 func validateMTU(mtu interface{}) error {
@@ -674,10 +673,10 @@ WHERE (profiles = $1::text[])
 	return serviceInterface, util.JoinErrs(errs), nil
 }
 
-func validateV3(s *tc.ServerV30, tx *sql.Tx) (string, error) {
+func validateV3(s *tc.ServerV30, tx *sql.Tx) (string, error, error) {
 
 	if len(s.Interfaces) == 0 {
-		return "", errors.New("a server must have at least one interface")
+		return "", errors.New("a server must have at least one interface"), nil
 	}
 	var errs []error
 	var serviceAddrV4Found bool
@@ -740,8 +739,10 @@ func validateV3(s *tc.ServerV30, tx *sql.Tx) (string, error) {
 		errs = append(errs, errors.New("a server must have at least one service address"))
 	}
 
-	if errs = append(errs, validateCommon(&s.CommonServerProperties, tx)...); errs != nil {
-		return serviceInterface, util.JoinErrs(errs)
+	commonErrs, sysErr := validateCommon(&s.CommonServerProperties, tx)
+	errs = append(errs, commonErrs...)
+	if len(errs) > 0 || sysErr != nil {
+		return serviceInterface, util.JoinErrs(errs), sysErr
 	}
 	query := `
 SELECT s.ID, ip.address FROM server s
@@ -760,7 +761,7 @@ and p.id = $1
 		rows, err = tx.Query(query, *s.ProfileID)
 	}
 	if err != nil {
-		errs = append(errs, errors.New("unable to determine service address uniqueness"))
+		return serviceInterface, util.JoinErrs(errs), fmt.Errorf("unable to determine service address uniqueness: querying: %w", err)
 	} else if rows != nil {
 		defer rows.Close()
 		for rows.Next() {
@@ -768,14 +769,14 @@ and p.id = $1
 			var ipaddress string
 			err = rows.Scan(&id, &ipaddress)
 			if err != nil {
-				errs = append(errs, errors.New("unable to determine service address uniqueness"))
+				return serviceInterface, util.JoinErrs(errs), fmt.Errorf("unable to determine service address uniqueness: scanning: %w", err)
 			} else if (ipaddress == ipv4 || ipaddress == ipv6) && (s.ID == nil || *s.ID != id) {
 				errs = append(errs, fmt.Errorf("there exists a server with id %v on the same profile that has the same service address %s", id, ipaddress))
 			}
 		}
 	}
 
-	return serviceInterface, util.JoinErrs(errs)
+	return serviceInterface, util.JoinErrs(errs), nil
 }
 
 // Read is the handler for GET requests to /servers.
@@ -1469,8 +1470,9 @@ func Update(w http.ResponseWriter, r *http.Request) {
 	originalStatusID := *original.StatusID
 
 	var server tc.ServerV40
-	var serverV3 tc.ServerV30
 	var statusLastUpdatedTime time.Time
+	server.ID = new(int)
+	*server.ID = inf.IntParams["id"]
 	if inf.Version.Major >= 4 {
 		if err := json.NewDecoder(r.Body).Decode(&server); err != nil {
 			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
@@ -1499,6 +1501,9 @@ func Update(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	} else if inf.Version.Major >= 3 {
+		var serverV3 tc.ServerV30
+		serverV3.ID = new(int)
+		*serverV3.ID = inf.IntParams["id"]
 		if err := json.NewDecoder(r.Body).Decode(&serverV3); err != nil {
 			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
 			return
@@ -1511,12 +1516,16 @@ func Update(w http.ResponseWriter, r *http.Request) {
 			serverV3.StatusLastUpdated = original.StatusLastUpdated
 			statusLastUpdatedTime = *original.StatusLastUpdated
 		}
-		_, err := validateV3(&serverV3, tx)
-		if err != nil {
-			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		_, userErr, sysErr := validateV3(&serverV3, tx)
+		if userErr != nil || sysErr != nil {
+			code := http.StatusBadRequest
+			if sysErr != nil {
+				code = http.StatusInternalServerError
+			}
+			api.HandleErr(w, r, tx, code, userErr, sysErr)
 			return
 		}
-		profileName, err := dbhelpers.UpdateServerProfileTableForV2V3(serverV3.ID, serverV3.Profile, (original.ProfileNames)[0], tx)
+		profileName, err := dbhelpers.UpdateServerProfileTableForV2V3(serverV3.ID, serverV3.ProfileID, (original.ProfileNames)[0], tx)
 		if err != nil {
 			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("failed to update server_profile: %w", err))
 			return
@@ -1532,16 +1541,22 @@ func Update(w http.ResponseWriter, r *http.Request) {
 		}
 	} else {
 		var legacyServer tc.ServerNullableV2
+		legacyServer.ID = new(int)
+		*legacyServer.ID = inf.IntParams["id"]
 		if err := json.NewDecoder(r.Body).Decode(&legacyServer); err != nil {
 			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
 			return
 		}
-		err := validateV2(&legacyServer, tx)
-		if err != nil {
-			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		userErr, sysErr := validateV2(&legacyServer, tx)
+		if userErr != nil || sysErr != nil {
+			code := http.StatusBadRequest
+			if sysErr != nil {
+				code = http.StatusInternalServerError
+			}
+			api.HandleErr(w, r, tx, code, userErr, sysErr)
 			return
 		}
-		profileName, err := dbhelpers.UpdateServerProfileTableForV2V3(legacyServer.ID, legacyServer.Profile, (original.ProfileNames)[0], tx)
+		profileName, err := dbhelpers.UpdateServerProfileTableForV2V3(legacyServer.ID, legacyServer.ProfileID, (original.ProfileNames)[0], tx)
 		if err != nil {
 			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("failed to update server_profile: %w", err))
 			return
@@ -1575,8 +1590,6 @@ func Update(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	server.ID = new(int)
-	*server.ID = inf.IntParams["id"]
 	status, ok, err := dbhelpers.GetStatusByID(*server.StatusID, tx)
 	if err != nil {
 		sysErr = fmt.Errorf("getting server #%d status (#%d): %v", id, *server.StatusID, err)
@@ -1807,8 +1820,8 @@ func insertServerProfile(id int, pName []string, tx *sql.Tx) (error, error, int)
 	}
 	insertQuery := `
 	INSERT INTO server_profile (
-		server, 
-		profile_name, 
+		server,
+		profile_name,
 		priority
 	)SELECT $1, profile_name, priority
 	FROM UNNEST($2::text[], $3::int[]) WITH ORDINALITY AS tmp(profile_name, priority)
@@ -1843,8 +1856,12 @@ func createV2(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 
 	server.XMPPID = newUUID()
 
-	if err := validateV2(&server, inf.Tx.Tx); err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
+	if userErr, sysErr := validateV2(&server, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		code := http.StatusBadRequest
+		if sysErr != nil {
+			code = http.StatusInternalServerError
+		}
+		api.HandleErr(w, r, inf.Tx.Tx, code, userErr, sysErr)
 		return
 	}
 
@@ -1980,9 +1997,13 @@ func createV3(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 
 	server.XMPPID = newUUID()
 
-	_, err := validateV3(&server, inf.Tx.Tx)
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
+	_, userErr, sysErr := validateV3(&server, inf.Tx.Tx)
+	if userErr != nil || sysErr != nil {
+		code := http.StatusBadRequest
+		if sysErr != nil {
+			code = http.StatusInternalServerError
+		}
+		api.HandleErr(w, r, inf.Tx.Tx, code, userErr, sysErr)
 		return
 	}
 
diff --git a/traffic_ops/traffic_ops_golang/server/servers_assignment.go b/traffic_ops/traffic_ops_golang/server/servers_assignment.go
index d9f0d87ac..0107a9db8 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_assignment.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_assignment.go
@@ -432,8 +432,7 @@ INSERT INTO parameter (config_file, name, value)
 	for rows.Next() {
 		var ID int64
 		if err := rows.Scan(&ID); err != nil {
-			log.Errorf("could not scan parameter ID: %s\n", err)
-			return nil, tc.DBError
+			return nil, fmt.Errorf("could not scan parameter ID: %w", err)
 		}
 		parameterIds = append(parameterIds, ID)
 	}
diff --git a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
index ef56814e2..64291f21c 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
@@ -112,14 +112,14 @@ func (ssc *TOServerServerCapability) GetType() string {
 	return "server server_capability"
 }
 
-// Validate fulfills the api.Validator interface
-func (ssc TOServerServerCapability) Validate() error {
+// Validate fulfills the api.Validator interface.
+func (ssc TOServerServerCapability) Validate() (error, error) {
 	errs := validation.Errors{
 		ServerQueryParam:           validation.Validate(ssc.ServerID, validation.Required),
 		ServerCapabilityQueryParam: validation.Validate(ssc.ServerCapability, validation.Required),
 	}
 
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (ssc *TOServerServerCapability) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
@@ -375,7 +375,7 @@ SELECT ARRAY(
 	SELECT dsrc.deliveryservice_id
 	FROM deliveryservices_required_capability as dsrc
 	WHERE deliveryservice_id IN (
-		SELECT deliveryservice 
+		SELECT deliveryservice
 		FROM deliveryservice_server
 		WHERE server = $1)
 	AND dsrc.required_capability = $2)`
diff --git a/traffic_ops/traffic_ops_golang/server/servers_update_status.go b/traffic_ops/traffic_ops_golang/server/servers_update_status.go
index b39554757..bdc2e9aa0 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_update_status.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_update_status.go
@@ -21,6 +21,7 @@ package server
 
 import (
 	"database/sql"
+	"fmt"
 	"net/http"
 
 	"github.com/lib/pq"
@@ -39,7 +40,7 @@ func GetServerUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
-	serverUpdateStatuses, err := getServerUpdateStatus(inf.Tx.Tx, inf.Config, inf.Params["host_name"])
+	serverUpdateStatuses, err, _ := getServerUpdateStatus(inf.Tx.Tx, inf.Config, inf.Params["host_name"])
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
@@ -55,7 +56,7 @@ func GetServerUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func getServerUpdateStatus(tx *sql.Tx, cfg *config.Config, hostName string) ([]tc.ServerUpdateStatusV40, error) {
+func getServerUpdateStatus(tx *sql.Tx, cfg *config.Config, hostName string) ([]tc.ServerUpdateStatusV40, error, error) {
 
 	updateStatuses := []tc.ServerUpdateStatusV40{}
 
@@ -86,7 +87,7 @@ UNION ALL
  * ancestor topology node found by topology_ancestors.
  */
 ), server_topology_ancestors AS (
-SELECT s.id, 
+SELECT s.id,
 	s.cachegroup,
 	s.cdn_id,
 	s.config_update_time > s.config_apply_time AS upd_pending,
@@ -99,7 +100,7 @@ SELECT s.id,
 	JOIN status ON status.id = s.status
 	WHERE status.name = ANY($1::TEXT[])
 ), parentservers AS (
-SELECT ps.id, 
+SELECT ps.id,
 	ps.cachegroup,
 	ps.cdn_id,
 	ps.config_update_time > ps.config_apply_time AS upd_pending,
@@ -160,7 +161,7 @@ ORDER BY s.id
 	rows, err := tx.Query(selectQuery, pq.Array(cacheStatusesToCheck), tc.UseRevalPendingParameterName, tc.GlobalConfigFileName, pq.Array(cacheGroupTypes), hostName)
 	if err != nil {
 		log.Errorf("could not execute query: %s\n", err)
-		return nil, tc.DBError
+		return nil, nil, fmt.Errorf("could not execute query: %w", err)
 	}
 	defer log.Close(rows, "getServerUpdateStatus(): unable to close db connection")
 
@@ -168,10 +169,9 @@ ORDER BY s.id
 		var us tc.ServerUpdateStatusV40
 		var serverType string
 		if err := rows.Scan(&us.HostId, &us.HostName, &serverType, &us.RevalPending, &us.UseRevalPending, &us.UpdatePending, &us.Status, &us.ParentPending, &us.ParentRevalPending, &us.ConfigUpdateTime, &us.ConfigApplyTime, &us.RevalidateUpdateTime, &us.RevalidateApplyTime); err != nil {
-			log.Errorf("could not scan server update status: %s\n", err)
-			return nil, tc.DBError
+			return nil, nil, fmt.Errorf("could not scan server update status: %w", err)
 		}
 		updateStatuses = append(updateStatuses, us)
 	}
-	return updateStatuses, nil
+	return updateStatuses, nil, nil
 }
diff --git a/traffic_ops/traffic_ops_golang/servercapability/servercapability.go b/traffic_ops/traffic_ops_golang/servercapability/servercapability.go
index 383c1cfaf..daffb5438 100644
--- a/traffic_ops/traffic_ops_golang/servercapability/servercapability.go
+++ b/traffic_ops/traffic_ops_golang/servercapability/servercapability.go
@@ -107,12 +107,12 @@ func (v *TOServerCapability) GetType() string {
 	return "server capability"
 }
 
-func (v *TOServerCapability) Validate() error {
+func (v *TOServerCapability) Validate() (error, error) {
 	rule := validation.NewStringRule(tovalidate.IsAlphanumericUnderscoreDash, "must consist of only alphanumeric, dash, or underscore characters")
 	errs := validation.Errors{
 		"name": validation.Validate(v.Name, validation.Required, rule),
 	}
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (v *TOServerCapability) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
diff --git a/traffic_ops/traffic_ops_golang/servicecategory/servicecategories.go b/traffic_ops/traffic_ops_golang/servicecategory/servicecategories.go
index 893eaf978..f3e46e96f 100644
--- a/traffic_ops/traffic_ops_golang/servicecategory/servicecategories.go
+++ b/traffic_ops/traffic_ops_golang/servicecategory/servicecategories.go
@@ -31,7 +31,7 @@ import (
 	"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/dbhelpers"
-	"github.com/go-ozzo/ozzo-validation"
+	validation "github.com/go-ozzo/ozzo-validation"
 )
 
 type TOServiceCategory struct {
@@ -91,12 +91,12 @@ func (serviceCategory *TOServiceCategory) SelectMaxLastUpdatedQuery(where, order
 	select max(last_updated) as t from last_deleted l where l.table_name='service_category') as res`
 }
 
-func (serviceCategory TOServiceCategory) Validate() error {
+func (serviceCategory TOServiceCategory) Validate() (error, error) {
 	nameRule := validation.NewStringRule(tovalidate.IsAlphanumericDash, "must consist of only alphanumeric or dash characters.")
 	errs := validation.Errors{
 		"name": validation.Validate(serviceCategory.Name, validation.Required, nameRule),
 	}
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (serviceCategory *TOServiceCategory) Create() (error, error, int) {
@@ -129,8 +129,12 @@ func Update(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if err := newSC.Validate(); err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
+	if userErr, sysErr := newSC.Validate(); userErr != nil || sysErr != nil {
+		code := http.StatusBadRequest
+		if sysErr != nil {
+			code = http.StatusInternalServerError
+		}
+		api.HandleErr(w, r, inf.Tx.Tx, code, userErr, sysErr)
 		return
 	}
 
diff --git a/traffic_ops/traffic_ops_golang/staticdnsentry/staticdnsentry.go b/traffic_ops/traffic_ops_golang/staticdnsentry/staticdnsentry.go
index 5030dc4e8..298964913 100644
--- a/traffic_ops/traffic_ops_golang/staticdnsentry/staticdnsentry.go
+++ b/traffic_ops/traffic_ops_golang/staticdnsentry/staticdnsentry.go
@@ -94,11 +94,11 @@ func (staticDNSEntry *TOStaticDNSEntry) SetKeys(keys map[string]interface{}) {
 	staticDNSEntry.ID = &i
 }
 
-// Validate fulfills the api.Validator interface
-func (staticDNSEntry TOStaticDNSEntry) Validate() error {
+// Validate fulfills the api.Validator interface.
+func (staticDNSEntry TOStaticDNSEntry) Validate() (error, error) {
 	typeStr, err := tc.ValidateTypeID(staticDNSEntry.ReqInfo.Tx.Tx, &staticDNSEntry.TypeID, "staticdnsentry")
 	if err != nil {
-		return err
+		return err, nil
 	}
 
 	var addressErr, ttlErr error
@@ -135,7 +135,7 @@ func (staticDNSEntry TOStaticDNSEntry) Validate() error {
 		"ttl":               ttlErr,
 		"typeId":            validation.Validate(staticDNSEntry.TypeID, validation.Required),
 	}
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (en *TOStaticDNSEntry) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
diff --git a/traffic_ops/traffic_ops_golang/status/statuses.go b/traffic_ops/traffic_ops_golang/status/statuses.go
index 875edce43..24f7bc38c 100644
--- a/traffic_ops/traffic_ops_golang/status/statuses.go
+++ b/traffic_ops/traffic_ops_golang/status/statuses.go
@@ -94,11 +94,11 @@ func (status TOStatus) GetAuditName() string {
 
 func (status TOStatus) GetType() string { return "status" }
 
-func (status TOStatus) Validate() error {
+func (status TOStatus) Validate() (error, error) {
 	errs := validation.Errors{
 		"name": validation.Validate(status.Name, validation.NotNil, validation.Required),
 	}
-	return util.JoinErrs(tovalidate.ToErrors(errs))
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 }
 
 func (st *TOStatus) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
diff --git a/traffic_ops/traffic_ops_golang/steeringtargets/steeringtargets.go b/traffic_ops/traffic_ops_golang/steeringtargets/steeringtargets.go
index 091d6cb9d..46023832c 100644
--- a/traffic_ops/traffic_ops_golang/steeringtargets/steeringtargets.go
+++ b/traffic_ops/traffic_ops_golang/steeringtargets/steeringtargets.go
@@ -94,7 +94,7 @@ func (st TOSteeringTargetV11) GetType() string {
 	return "steeringtarget"
 }
 
-func (st TOSteeringTargetV11) Validate() error {
+func (st TOSteeringTargetV11) Validate() (error, error) {
 	return st.SteeringTargetNullable.Validate(st.ReqInfo.Tx.Tx)
 }
 
diff --git a/traffic_ops/traffic_ops_golang/topology/topologies.go b/traffic_ops/traffic_ops_golang/topology/topologies.go
index f9fb81dea..df9ead8bb 100644
--- a/traffic_ops/traffic_ops_golang/topology/topologies.go
+++ b/traffic_ops/traffic_ops_golang/topology/topologies.go
@@ -89,7 +89,7 @@ func (topology *TOTopology) GetType() string {
 }
 
 // Validate is a requirement of the api.Validator interface.
-func (topology *TOTopology) Validate() error {
+func (topology *TOTopology) Validate() (error, error) {
 	currentTopoName := topology.APIInfoImpl.ReqInfo.Params["name"]
 	nameRule := validation.NewStringRule(tovalidate.IsAlphanumericUnderscoreDash, "must consist of only alphanumeric, dash, or underscore characters.")
 	rules := validation.Errors{}
@@ -117,12 +117,7 @@ func (topology *TOTopology) Validate() error {
 		cacheGroupNames[index] = node.Cachegroup
 	}
 	if cacheGroupMap, userErr, sysErr, _ = cachegroup.GetCacheGroupsByName(cacheGroupNames, topology.APIInfoImpl.ReqInfo.Tx); userErr != nil || sysErr != nil {
-		var err error
-		message := "could not get cachegroups"
-		if userErr != nil {
-			err = fmt.Errorf("%s: %s", message, userErr.Error())
-		}
-		return err
+		return userErr, sysErr
 	}
 	cacheGroups = make([]tc.CacheGroupNullable, len(topology.Nodes))
 	for index, node := range topology.Nodes {
@@ -133,7 +128,7 @@ func (topology *TOTopology) Validate() error {
 	}
 	rules["duplicate cachegroup name"] = checkUniqueCacheGroupNames(topology.Nodes)
 	if !cacheGroupsExist {
-		return util.JoinErrs(tovalidate.ToErrors(rules))
+		return util.JoinErrs(tovalidate.ToErrors(rules)), nil
 	}
 
 	for index, node := range topology.Nodes {
@@ -146,8 +141,7 @@ func (topology *TOTopology) Validate() error {
 	}
 	dsCDNs, err := dbhelpers.GetDeliveryServiceCDNsByTopology(topology.ReqInfo.Tx.Tx, currentTopoName)
 	if err != nil {
-		log.Errorf("validating topology: %v", err)
-		return errors.New("unable to validate topology")
+		return errors.New("unable to validate topology"), fmt.Errorf("validating Topology: %w", err)
 	}
 	rules["empty cachegroups"] = topology_validation.CheckForEmptyCacheGroups(topology.ReqInfo.Tx, cacheGroupIds, dsCDNs, false, nil)
 	rules["required capabilities"] = topology.validateDSRequiredCapabilities(currentTopoName)
@@ -155,17 +149,13 @@ func (topology *TOTopology) Validate() error {
 	//Get current Topology-CG for the requested change.
 	topoCachegroupNames := topology.getCachegroupNames()
 	userErr, sysErr, _ = dbhelpers.CheckTopologyOrgServerCGInDSCG(topology.ReqInfo.Tx.Tx, dsCDNs, currentTopoName, topoCachegroupNames)
-	if userErr != nil {
-		return userErr
-	}
-	if sysErr != nil {
-		log.Errorf("error while validate topology: %s", sysErr.Error())
-		return errors.New("unable to validate topology")
+	if userErr != nil || sysErr != nil {
+		return userErr, sysErr
 	}
 
 	/* Only perform further checks if everything so far is valid */
 	if err = util.JoinErrs(tovalidate.ToErrors(rules)); err != nil {
-		return err
+		return err, nil
 	}
 
 	for _, leafMid := range checkForLeafMids(topology.Nodes, cacheGroups) {
@@ -174,7 +164,7 @@ func (topology *TOTopology) Validate() error {
 	_, rules["topology cycles"] = checkForCycles(topology.Nodes)
 	rules["super-topology cycles"] = topology.checkForCyclesAcrossTopologies()
 
-	return util.JoinErrs(tovalidate.ToErrors(rules))
+	return util.JoinErrs(tovalidate.ToErrors(rules)), nil
 }
 
 func (topology *TOTopology) nodesInOtherTopologies() ([]tc.TopologyNode, map[string][]string, error) {
diff --git a/traffic_ops/traffic_ops_golang/types/types.go b/traffic_ops/traffic_ops_golang/types/types.go
index 8229779ec..f16051c72 100644
--- a/traffic_ops/traffic_ops_golang/types/types.go
+++ b/traffic_ops/traffic_ops_golang/types/types.go
@@ -94,16 +94,16 @@ func (typ *TOType) GetType() string {
 	return "type"
 }
 
-func (typ *TOType) Validate() error {
+func (typ *TOType) Validate() (error, error) {
 	errs := validation.Errors{
 		"name":         validation.Validate(typ.Name, validation.Required),
 		"description":  validation.Validate(typ.Description, validation.Required),
 		"use_in_table": validation.Validate(typ.UseInTable, validation.Required),
 	}
 	if errs != nil {
-		return util.JoinErrs(tovalidate.ToErrors(errs))
+		return util.JoinErrs(tovalidate.ToErrors(errs)), nil
 	}
-	return nil
+	return nil, nil
 }
 
 func (tp *TOType) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
diff --git a/traffic_ops/traffic_ops_golang/user/user.go b/traffic_ops/traffic_ops_golang/user/user.go
index 8cd9df7b7..89b3fea9a 100644
--- a/traffic_ops/traffic_ops_golang/user/user.go
+++ b/traffic_ops/traffic_ops_golang/user/user.go
@@ -40,7 +40,7 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
 
-	"github.com/go-ozzo/ozzo-validation"
+	validation "github.com/go-ozzo/ozzo-validation"
 	"github.com/go-ozzo/ozzo-validation/is"
 	"github.com/jmoiron/sqlx"
 )
@@ -97,7 +97,7 @@ func (user *TOUser) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
 	}
 }
 
-func (user *TOUser) Validate() error {
+func (user *TOUser) Validate() (error, error) {
 
 	validateErrs := validation.Errors{
 		"email":    validation.Validate(user.Email, validation.Required, is.Email),
@@ -111,11 +111,11 @@ func (user *TOUser) Validate() error {
 	if user.LocalPassword != nil {
 		_, err := auth.IsGoodLoginPair(*user.Username, *user.LocalPassword)
 		if err != nil {
-			return err
+			return err, nil
 		}
 	}
 
-	return util.JoinErrs(tovalidate.ToErrors(validateErrs))
+	return util.JoinErrs(tovalidate.ToErrors(validateErrs)), nil
 }
 
 func (user *TOUser) postValidate() error {
diff --git a/traffic_ops/v3-client/server.go b/traffic_ops/v3-client/server.go
index 13b2fad97..c7d026c3e 100644
--- a/traffic_ops/v3-client/server.go
+++ b/traffic_ops/v3-client/server.go
@@ -204,6 +204,10 @@ func (to *Session) GetFirstServer(params *url.Values, header http.Header) (tc.Se
 	return firstServer, reqInf, err
 }
 
+// GetServerDetailsByHostNameWithHdr retrieves the "details" of all servers with
+// the given hostname.
+// Deprecated: Server "details" as a concept have been removed from the latest
+// version of the API, and clients should use GetServersWithHdr instead.
 func (to *Session) GetServerDetailsByHostNameWithHdr(hostName string, header http.Header) ([]tc.ServerDetailV30, toclientlib.ReqInf, error) {
 	v := url.Values{}
 	v.Add("hostName", hostName)
diff --git a/traffic_ops/v4-client/server.go b/traffic_ops/v4-client/server.go
index 59d300948..ab56c42c4 100644
--- a/traffic_ops/v4-client/server.go
+++ b/traffic_ops/v4-client/server.go
@@ -29,9 +29,6 @@ const (
 	// apiServers is the API version-relative path to the /servers API
 	// endpoint.
 	apiServers = "/servers"
-	// apiServersDetails is the API version-relative path to the
-	// /servers/details API endpoint.
-	apiServersDetails = "/servers/details"
 )
 
 func needAndCanFetch(id *int, name *string) bool {
@@ -127,14 +124,6 @@ func (to *Session) GetServers(opts RequestOptions) (tc.ServersV4Response, toclie
 	return data, reqInf, err
 }
 
-// GetServersDetails retrieves the Server Details of the Server with the given
-// (short) Hostname.
-func (to *Session) GetServersDetails(opts RequestOptions) (tc.ServersV4DetailResponse, toclientlib.ReqInf, error) {
-	var data tc.ServersV4DetailResponse
-	reqInf, err := to.get(apiServersDetails, opts, &data)
-	return data, reqInf, err
-}
-
 // DeleteServer deletes the Server with the given ID.
 func (to *Session) DeleteServer(id int, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	route := fmt.Sprintf("%s/%d", apiServers, id)


Test patch:
diff --git a/lib/go-atscfg/strategiesdotconfig_test.go b/lib/go-atscfg/strategiesdotconfig_test.go
index d39c480de..073fa4f90 100644
--- a/lib/go-atscfg/strategiesdotconfig_test.go
+++ b/lib/go-atscfg/strategiesdotconfig_test.go
@@ -445,4 +445,9 @@ func TestMakeStrategiesHTTPSOrigin(t *testing.T) {
 	if strings.Contains(txt, "port: 443") {
 		t.Errorf("expected edge parent.config of https origin to use internal http port 80 and not https/443, actual: '%v'", txt)
 	}
+
+	// checks for properly-formed merge keys
+	if strings.Contains(cfg.Text, "<< ") {
+		t.Errorf("expected yaml merge keys to be '<<: ', actual malformed '<< ': %v", cfg.Text)
+	}
 }
diff --git a/test/router/index.html b/test/router/index.html
index e4dbdee01..33ea56135 100644
--- a/test/router/index.html
+++ b/test/router/index.html
@@ -24,14 +24,47 @@ under the License.
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <title>Traffic Router Loadtest</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.6/d3.min.js"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.15.1/moment.min.js"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react.js"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react-dom.js"></script>
-    <script src="https://unpkg.com/babel-core@5.8.38/browser.min.js"></script>
-    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
-    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
-    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
+    <script
+        integrity="sha384-HERpnF+tlZ/PxhCuxaUfTFrU8tKctj33vfD60o0LCNaYDxhrZsk45o1QG9L8GV1y"
+        crossorigin="anonymous"
+        src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.6/d3.min.js"
+    ></script>
+    <script
+        integrity="sha384-7pfELK0arQ3VANqV4kiPWPh5wOsrfitjFGF/NdyHXUJ3JJPy/rNhasPtdkaNKhul"
+        crossorigin="anonymous"
+        src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.15.1/moment.min.js"
+    ></script>
+    <script
+        integrity="sha384-bQIyvl+8Ufi5KiKZPG9VItNWmhcAXA1pa5nHIEoBGob+rdbjJnpNV3s288Mz2yZu"
+        crossorigin="anonymous"
+        src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react.js"
+    ></script>
+    <script
+        integrity="sha384-ZzFfcTbsRst34N23lWs6TtlfonXwDgpeALh+ObwYXav5BSo0j7KsaAtcdn+xrnS1"
+        crossorigin="anonymous"
+        src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react-dom.js"
+    ></script>
+    <script
+        integrity="sha384-Fw04PJPC6ayYz0V6lPpRaiQxouxskHt/AjlelA2NWboTMnn5niXpXMyN4hftijud"
+        crossorigin="anonymous"
+        src="https://unpkg.com/babel-core@5.8.38/browser.min.js"
+    ></script>
+    <link
+        integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
+        crossorigin="anonymous"
+        rel="stylesheet"
+        href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
+    >
+    <script
+        integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ"
+        crossorigin="anonymous"
+        src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"
+    ></script>
+    <script
+        integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
+        crossorigin="anonymous"
+        src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
+    ></script>
     <link href="css/loadtest.css" rel="stylesheet">
     <script src="js/load-test.jsx" type="text/babel"></script>
 </head>
diff --git a/test/router/js/load-test.jsx b/test/router/js/load-test.jsx
index f06a6dd7a..32888a7ed 100644
--- a/test/router/js/load-test.jsx
+++ b/test/router/js/load-test.jsx
@@ -12,29 +12,41 @@
 	limitations under the License.
 */}
 
-if ( ! Array.prototype.groupBy) {
-    Array.prototype.groupBy = function (f)
-    {
-        var groups = {};
-        this.forEach(function(o) {
-            var group = JSON.stringify(f(o));
-            groups[group] = groups[group] || [];
-            groups[group].push(o);
-        });
-
-        return Object.keys(groups).map(function (group) {
-            return groups[group];
-        });
-    };
+/**
+ * Creates an array of arrays of elements that return the same value from the
+ * given function.
+ *
+ * @template T
+ * @param {Array<T>} a The array to group.
+ * @param {(e: T) => string} f The grouping function; values in `a` that return
+ * the same value from this function will be grouped together.
+ * @returns {Array<Array<T>>} All of the elements of `a`, grouped by `f`.
+ */
+function groupBy(a, f) {
+    /** @type Record<string, Array<T>> */
+    const groups = {};
+    for (const element of a) {
+        const group = f(element);
+        if (group in groups) {
+            groups[group].push(element);
+        } else {
+            groups[group] = [element];
+        }
+    }
+    return Object.keys(groups).map(g=>groups[g]);
 }
 
-if (!Array.prototype.unique) {
-    Array.prototype.unique = function(a){
-        return function() {
-            return this.filter(a);
-        }
-    }(function (a,b,c) { return c.indexOf(a, b + 1) < 0; }
-    );
+/**
+ * Gets the unique elements of an Array as a new Array. Does not modify Arrays
+ * in-place.
+ *
+ * @template T
+ * @param {Array<T>} a The array to be unique'd.
+ * @returns {Array<T>} A new array with all of the values in `a`, but without
+ * any duplicate values. Duplicates are determined with `===`.
+ */
+function unique(a) {
+    return Array.from(new Set(a));
 }
 
 var CdnTestForm = React.createClass({
@@ -315,8 +327,7 @@ var LabBox = React.createClass({
 
 
         if (this.deliveryServices != null) {
-            var cdnNames = this.deliveryServices.map(function (item) { return item.cdnName})
-                .unique()
+            var cdnNames = unique(this.deliveryServices.map(function (item) { return item.cdnName}))
                 .filter(function(item) { return item.toLowerCase() != 'all'});
 
             cdnNames.forEach(function (cdnName) {
@@ -514,9 +525,7 @@ var ResultsList = React.createClass({
         }
 
         histogram(this.props.data.map(function (r, i) { return (r.latency / 1000.0).toFixed(3)}));
-        var groupedResults = this.props.data.groupBy(function (result) {
-           return [result.host];
-        });
+        var groupedResults = groupBy(this.props.data, result => JSON.stringify([result.host]));
 
         for (var i = 0; i < groupedResults.length; i++) {
             groupedResults[i].host = groupedResults[i][0].host;
diff --git a/traffic_ops/testing/api/v4/servers_test.go b/traffic_ops/testing/api/v4/servers_test.go
index 4adf0d834..a6f1680a4 100644
--- a/traffic_ops/testing/api/v4/servers_test.go
+++ b/traffic_ops/testing/api/v4/servers_test.go
@@ -42,7 +42,6 @@ func TestServers(t *testing.T) {
 		header.Set(rfc.IfUnmodifiedSince, time)
 		UpdateTestServers(t)
 		UpdateTestServersWithHeaders(t, header)
-		GetTestServersDetails(t)
 		GetTestServers(t)
 		GetTestServersIMSAfterChange(t, header)
 		GetTestServersQueryParameters(t)
@@ -657,38 +656,6 @@ func GetTestServers(t *testing.T) {
 	}
 }
 
-func GetTestServersDetails(t *testing.T) {
-	opts := client.NewRequestOptions()
-	for _, server := range testData.Servers {
-		if server.HostName == nil {
-			t.Errorf("found server with nil hostname: %+v", server)
-			continue
-		}
-		opts.QueryParameters.Set("hostName", *server.HostName)
-		resp, _, err := TOSession.GetServersDetails(opts)
-		if err != nil {
-			t.Errorf("cannot get Server Details: %v - alerts: %+v", err, resp.Alerts)
-		}
-		if len(resp.Response) == 0 {
-			t.Fatal("no servers in response, quitting")
-		}
-		if len(resp.Response[0].ServerInterfaces) == 0 {
-			t.Fatalf("no interfaces to check, quitting")
-		}
-		if len(server.Interfaces) == 0 {
-			t.Fatalf("no interfaces to check, quitting")
-		}
-
-		// just check the first interface for noe
-		if resp.Response[0].ServerInterfaces[0].RouterHostName != server.Interfaces[0].RouterHostName {
-			t.Errorf("expected router host name to be %s, but got %s", server.Interfaces[0].RouterHostName, resp.Response[0].ServerInterfaces[0].RouterHostName)
-		}
-		if resp.Response[0].ServerInterfaces[0].RouterPortName != server.Interfaces[0].RouterPortName {
-			t.Errorf("expected router port to be %s, but got %s", server.Interfaces[0].RouterPortName, resp.Response[0].ServerInterfaces[0].RouterPortName)
-		}
-	}
-}
-
 func GetTestServersQueryParameters(t *testing.T) {
 	dses, _, err := TOSession.GetDeliveryServices(client.RequestOptions{QueryParameters: url.Values{"xmlId": []string{"ds1"}}})
 	if err != nil {
diff --git a/traffic_ops/traffic_ops_golang/api/shared_handlers_test.go b/traffic_ops/traffic_ops_golang/api/shared_handlers_test.go
index 2a26e18f3..51675ee5b 100644
--- a/traffic_ops/traffic_ops_golang/api/shared_handlers_test.go
+++ b/traffic_ops/traffic_ops_golang/api/shared_handlers_test.go
@@ -72,11 +72,11 @@ func (i *tester) GetAuditName() string {
 }
 
 //Validator interface function
-func (v *tester) Validate() error {
+func (v *tester) Validate() (error, error) {
 	if v.ID < 1 {
-		return errors.New("ID is too low")
+		return errors.New("ID is too low"), nil
 	}
-	return nil
+	return nil, nil
 }
 
 //Creator interface functions
diff --git a/traffic_ops/traffic_ops_golang/asn/asns_test.go b/traffic_ops/traffic_ops_golang/asn/asns_test.go
index 67410c50f..62a46af7f 100644
--- a/traffic_ops/traffic_ops_golang/asn/asns_test.go
+++ b/traffic_ops/traffic_ops_golang/asn/asns_test.go
@@ -125,7 +125,8 @@ func TestValidate(t *testing.T) {
 		api.APIInfoImpl{},
 		tc.ASNNullable{ASN: &i, CachegroupID: &i},
 	}
-	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(asn.Validate())))
+	err, _ := asn.Validate()
+	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(err)))
 	expected := util.JoinErrsStr([]error{
 		errors.New(`'asn' must be no less than 0`),
 		errors.New(`'cachegroupId' must be no less than 0`),
diff --git a/traffic_ops/traffic_ops_golang/cachegroup/cachegroups_test.go b/traffic_ops/traffic_ops_golang/cachegroup/cachegroups_test.go
index ae3c70110..601f05af1 100644
--- a/traffic_ops/traffic_ops_golang/cachegroup/cachegroups_test.go
+++ b/traffic_ops/traffic_ops_golang/cachegroup/cachegroups_test.go
@@ -231,7 +231,8 @@ func TestValidate(t *testing.T) {
 			LastUpdated:         &lu,
 		},
 	}
-	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(c.Validate())))
+	err, _ = c.Validate()
+	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(err)))
 
 	expectedErrs := util.JoinErrsStr([]error{
 		errors.New(`'latitude' Must be a floating point number within the range +-90`),
@@ -272,9 +273,12 @@ func TestValidate(t *testing.T) {
 			LastUpdated:         &lu,
 		},
 	}
-	err = c.Validate()
+	err, sysErr := c.Validate()
 	if err != nil {
-		t.Errorf("expected nil, got %s", err)
+		t.Errorf("expected nil user error, got: %s", err)
+	}
+	if sysErr != nil {
+		t.Errorf("expected nil system error, got: %s", sysErr)
 	}
 }
 
diff --git a/traffic_ops/traffic_ops_golang/cdn/cdns_test.go b/traffic_ops/traffic_ops_golang/cdn/cdns_test.go
index 7830f550e..baa10197c 100644
--- a/traffic_ops/traffic_ops_golang/cdn/cdns_test.go
+++ b/traffic_ops/traffic_ops_golang/cdn/cdns_test.go
@@ -136,7 +136,8 @@ func TestValidate(t *testing.T) {
 	// invalid name, empty domainname
 	n := "not_a_valid_cdn"
 	c := TOCDN{CDNNullable: tc.CDNNullable{Name: &n}}
-	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(c.Validate())))
+	err, _ := c.Validate()
+	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(err)))
 
 	expectedErrs := util.JoinErrsStr([]error{
 		errors.New(`'domainName' cannot be blank`),
@@ -151,7 +152,7 @@ func TestValidate(t *testing.T) {
 	n = "This.is.2.a-Valid---CDNNAME."
 	d := `awesome-cdn.example.net`
 	c = TOCDN{CDNNullable: tc.CDNNullable{Name: &n, DomainName: &d}}
-	err := c.Validate()
+	err, _ = c.Validate()
 	if err != nil {
 		t.Errorf("expected nil, got %s", err)
 	}
diff --git a/traffic_ops/traffic_ops_golang/coordinate/coordinates_test.go b/traffic_ops/traffic_ops_golang/coordinate/coordinates_test.go
index 330af9c63..0fa8bb597 100644
--- a/traffic_ops/traffic_ops_golang/coordinate/coordinates_test.go
+++ b/traffic_ops/traffic_ops_golang/coordinate/coordinates_test.go
@@ -148,7 +148,8 @@ func TestValidate(t *testing.T) {
 		Longitude:   &lo,
 		LastUpdated: &lu,
 	}}
-	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(c.Validate())))
+	err, _ := c.Validate()
+	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(err)))
 
 	expectedErrs := util.JoinErrsStr([]error{
 		errors.New(`'latitude' Must be a floating point number within the range +-90`),
@@ -170,7 +171,7 @@ func TestValidate(t *testing.T) {
 		Longitude:   &lo,
 		LastUpdated: &lu,
 	}}
-	err := c.Validate()
+	err, _ = c.Validate()
 	if err != nil {
 		t.Errorf("expected nil, got %s", err)
 	}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments_test.go
index b16ac6c8b..de7f41b53 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments_test.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/comment/comments_test.go
@@ -69,7 +69,8 @@ func TestInterfaces(t *testing.T) {
 
 func TestValidate(t *testing.T) {
 	c := TODeliveryServiceRequestComment{}
-	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(c.Validate())))
+	err, _ := c.Validate()
+	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(err)))
 
 	expectedErrs := util.JoinErrsStr([]error{
 		errors.New(`'deliveryServiceRequestId' is required`),
@@ -84,7 +85,7 @@ func TestValidate(t *testing.T) {
 	d := 1
 	c = TODeliveryServiceRequestComment{DeliveryServiceRequestCommentNullable: tc.DeliveryServiceRequestCommentNullable{DeliveryServiceRequestID: &d, Value: &v}}
 
-	err := c.Validate()
+	err, _ = c.Validate()
 	if err != nil {
 		t.Errorf("expected nil, got %s", err)
 	}
diff --git a/traffic_ops/traffic_ops_golang/division/divisions_test.go b/traffic_ops/traffic_ops_golang/division/divisions_test.go
index 4f7b050fd..1e683510c 100644
--- a/traffic_ops/traffic_ops_golang/division/divisions_test.go
+++ b/traffic_ops/traffic_ops_golang/division/divisions_test.go
@@ -111,7 +111,8 @@ func TestInterfaces(t *testing.T) {
 
 func TestValidation(t *testing.T) {
 	div := TODivision{}
-	errs := test.SortErrors(test.SplitErrors(div.Validate()))
+	err, _ := div.Validate()
+	errs := test.SortErrors(test.SplitErrors(err))
 	expected := []error{}
 
 	if reflect.DeepEqual(expected, errs) {
diff --git a/traffic_ops/traffic_ops_golang/origin/origins_test.go b/traffic_ops/traffic_ops_golang/origin/origins_test.go
index f072527ca..a4e1220ab 100644
--- a/traffic_ops/traffic_ops_golang/origin/origins_test.go
+++ b/traffic_ops/traffic_ops_golang/origin/origins_test.go
@@ -186,7 +186,8 @@ func TestValidate(t *testing.T) {
 		FQDN:              nil,
 		Protocol:          nil,
 	}}
-	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(c.Validate())))
+	err, _ := c.Validate()
+	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(err)))
 
 	expectedErrs := util.JoinErrsStr([]error{
 		errors.New(`'deliveryServiceId' is required`),
@@ -218,7 +219,7 @@ func TestValidate(t *testing.T) {
 		Protocol:          &pro,
 		LastUpdated:       &lu,
 	}}
-	err := c.Validate()
+	err, _ = c.Validate()
 	if err != nil {
 		t.Errorf("expected nil, got %s", err)
 	}
@@ -323,7 +324,8 @@ func TestValidate(t *testing.T) {
 				c.IP6Address = &tc.Str
 				value = tc.Str
 			}
-			errStr := util.JoinErrsStr(test.SortErrors(test.SplitErrors(c.Validate())))
+			err, _ = c.Validate()
+			errStr := util.JoinErrsStr(test.SortErrors(test.SplitErrors(err)))
 			if !reflect.DeepEqual(util.JoinErrsStr(tc.ExpectedErrors), errStr) {
 				t.Errorf("given: '%v', expected %s, got %s", value, tc.ExpectedErrors, errStr)
 			}
diff --git a/traffic_ops/traffic_ops_golang/physlocation/phys_locations_test.go b/traffic_ops/traffic_ops_golang/physlocation/phys_locations_test.go
index 67d0eb99c..727183f45 100644
--- a/traffic_ops/traffic_ops_golang/physlocation/phys_locations_test.go
+++ b/traffic_ops/traffic_ops_golang/physlocation/phys_locations_test.go
@@ -134,8 +134,8 @@ func TestInterfaces(t *testing.T) {
 }
 
 func TestValidate(t *testing.T) {
-	p := TOPhysLocation{}
-	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(p.Validate())))
+	err, _ := (&TOPhysLocation{}).Validate()
+	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(err)))
 	expected := util.JoinErrsStr(test.SortErrors([]error{
 		errors.New("'state' cannot be blank"),
 		errors.New("'zip' cannot be blank"),
diff --git a/traffic_ops/traffic_ops_golang/profile/profiles_test.go b/traffic_ops/traffic_ops_golang/profile/profiles_test.go
index 26a98dc4c..ebd6e8694 100644
--- a/traffic_ops/traffic_ops_golang/profile/profiles_test.go
+++ b/traffic_ops/traffic_ops_golang/profile/profiles_test.go
@@ -138,7 +138,8 @@ func TestInterfaces(t *testing.T) {
 
 func TestValidate(t *testing.T) {
 	p := TOProfile{}
-	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(p.Validate())))
+	err, _ := p.Validate()
+	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(err)))
 	expected := util.JoinErrsStr(test.SortErrors([]error{
 		errors.New("'cdn' cannot be blank"),
 		errors.New("'description' cannot be blank"),
@@ -159,7 +160,7 @@ func TestValidate(t *testing.T) {
 	p.Type = new(string)
 	*p.Type = "type"
 
-	err := p.Validate()
+	err, _ = p.Validate()
 	if !strings.Contains(err.Error(), "cannot contain spaces") {
 		t.Error("Expected an error about the Profile name containing spaces")
 	}
diff --git a/traffic_ops/traffic_ops_golang/region/regions_test.go b/traffic_ops/traffic_ops_golang/region/regions_test.go
index 27ea29f4f..d34d337e2 100644
--- a/traffic_ops/traffic_ops_golang/region/regions_test.go
+++ b/traffic_ops/traffic_ops_golang/region/regions_test.go
@@ -118,7 +118,8 @@ func TestValidation(t *testing.T) {
 		LastUpdated:  tc.TimeNoMod{Time: time.Now()},
 	}
 	testTORegion := TORegion{Region: testRegion}
-	errs := test.SortErrors(test.SplitErrors(testTORegion.Validate()))
+	err, _ := testTORegion.Validate()
+	errs := test.SortErrors(test.SplitErrors(err))
 
 	if len(errs) > 0 {
 		t.Errorf(`expected no errors,  got %v`, errs)
@@ -130,7 +131,8 @@ func TestValidation(t *testing.T) {
 		LastUpdated: tc.TimeNoMod{Time: time.Now()},
 	}
 	testTORegionNoDivision := TORegion{Region: testRegionNoDivision}
-	errs = test.SortErrors(test.SplitErrors(testTORegionNoDivision.Validate()))
+	err, _ = testTORegionNoDivision.Validate()
+	errs = test.SortErrors(test.SplitErrors(err))
 	if len(errs) == 0 {
 		t.Errorf(`expected an error with a nil division id, received no error`)
 	} else {
diff --git a/traffic_ops/traffic_ops_golang/role/roles_test.go b/traffic_ops/traffic_ops_golang/role/roles_test.go
index bc6b508c6..4d65a421c 100644
--- a/traffic_ops/traffic_ops_golang/role/roles_test.go
+++ b/traffic_ops/traffic_ops_golang/role/roles_test.go
@@ -86,7 +86,8 @@ func TestValidate(t *testing.T) {
 		APIInfoImpl: api.APIInfoImpl{ReqInfo: &reqInfo},
 		Role:        role,
 	}
-	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(r.Validate())))
+	userErr, _ := r.Validate()
+	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(userErr)))
 
 	expectedErrs := util.JoinErrsStr([]error{
 		errors.New(`'description' cannot be blank`),
@@ -106,9 +107,12 @@ func TestValidate(t *testing.T) {
 		APIInfoImpl: api.APIInfoImpl{ReqInfo: &reqInfo},
 		Role:        role,
 	}
-	err := r.Validate()
-	if err != nil {
-		t.Errorf("expected nil, got %s", err)
+	userErr, sysErr := r.Validate()
+	if userErr != nil {
+		t.Errorf("expected nil user error, got: %s", userErr)
+	}
+	if sysErr != nil {
+		t.Errorf("expected nil system error, got: %v", sysErr)
 	}
 
 }
diff --git a/traffic_ops/traffic_ops_golang/server/servers_test.go b/traffic_ops/traffic_ops_golang/server/servers_test.go
index 94b33aa2a..828328414 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_test.go
@@ -575,7 +575,7 @@ func TestV3Validations(t *testing.T) {
 
 	tx := db.MustBegin().Tx
 
-	_, err = validateV3(&testServer, tx)
+	_, err, _ = validateV3(&testServer, tx)
 	if err != nil {
 		t.Errorf("Unexpected error validating test server: %v", err)
 	}
@@ -587,7 +587,7 @@ func TestV3Validations(t *testing.T) {
 	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
 	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
 
-	_, err = validateV3(&testServer, tx)
+	_, err, _ = validateV3(&testServer, tx)
 	if err == nil {
 		t.Errorf("Expected a server with no interfaces to be invalid")
 	} else {
@@ -601,7 +601,7 @@ func TestV3Validations(t *testing.T) {
 	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
 	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
 
-	_, err = validateV3(&testServer, tx)
+	_, err, _ = validateV3(&testServer, tx)
 	if err == nil {
 		t.Errorf("Expected a server with nil interfaces to be invalid")
 	} else {
@@ -618,7 +618,7 @@ func TestV3Validations(t *testing.T) {
 	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
 	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
 
-	_, err = validateV3(&testServer, tx)
+	_, err, _ = validateV3(&testServer, tx)
 	if err == nil {
 		t.Errorf("Expected a server an MTU < 1280 to be invalid")
 	} else {
@@ -634,7 +634,7 @@ func TestV3Validations(t *testing.T) {
 	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
 	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
 
-	_, err = validateV3(&testServer, tx)
+	_, err, _ = validateV3(&testServer, tx)
 	if err == nil {
 		t.Errorf("Expected a server with no IP addresses to be invalid")
 	} else {
@@ -649,7 +649,7 @@ func TestV3Validations(t *testing.T) {
 	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
 	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
 
-	_, err = validateV3(&testServer, tx)
+	_, err, _ = validateV3(&testServer, tx)
 	if err == nil {
 		t.Errorf("Expected a server with nil IP addresses to be invalid")
 	} else {
@@ -670,7 +670,7 @@ func TestV3Validations(t *testing.T) {
 	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
 	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
 
-	_, err = validateV3(&testServer, tx)
+	_, err, _ = validateV3(&testServer, tx)
 	if err == nil {
 		t.Errorf("Expected a server with no service addresses to be invalid")
 	} else {
@@ -684,7 +684,7 @@ func TestV3Validations(t *testing.T) {
 	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
 	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
 
-	_, err = validateV3(&testServer, tx)
+	_, err, _ = validateV3(&testServer, tx)
 	if err == nil {
 		t.Errorf("Expected a server with too many interfaces with service addresses to be invalid")
 	} else {
@@ -704,7 +704,7 @@ func TestV3Validations(t *testing.T) {
 	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
 	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
 
-	_, err = validateV3(&testServer, tx)
+	_, err, _ = validateV3(&testServer, tx)
 	if err == nil {
 		t.Errorf("Expected a server with no service addresses to be invalid")
 	} else {
diff --git a/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go b/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go
index 6f20fc6c2..bdcbf4e91 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go
@@ -58,7 +58,7 @@ func TestGetServerUpdateStatus(t *testing.T) {
 	}
 	defer tx.Commit()
 
-	result, err := getServerUpdateStatus(tx, &config.Config{ConfigTrafficOpsGolang: config.ConfigTrafficOpsGolang{DBQueryTimeoutSeconds: 20}}, "host_name_1")
+	result, err, _ := getServerUpdateStatus(tx, &config.Config{ConfigTrafficOpsGolang: config.ConfigTrafficOpsGolang{DBQueryTimeoutSeconds: 20}}, "host_name_1")
 	if err != nil {
 		t.Errorf("getServerUpdateStatus: %v", err)
 	}
diff --git a/traffic_ops/traffic_ops_golang/staticdnsentry/staticdnsentry_test.go b/traffic_ops/traffic_ops_golang/staticdnsentry/staticdnsentry_test.go
index c0c34eaf9..40a08c45d 100644
--- a/traffic_ops/traffic_ops_golang/staticdnsentry/staticdnsentry_test.go
+++ b/traffic_ops/traffic_ops_golang/staticdnsentry/staticdnsentry_test.go
@@ -89,7 +89,8 @@ func TestValidate(t *testing.T) {
 	reqInfo := api.APIInfo{Tx: tx}
 	// invalid name, empty domainname
 	ts := TOStaticDNSEntry{APIInfoImpl: api.APIInfoImpl{ReqInfo: &reqInfo}}
-	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(ts.Validate())))
+	err, _ = ts.Validate()
+	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(err)))
 
 	expectedErrs := util.JoinErrsStr([]error{
 		errors.New(`'address' cannot be blank`),
diff --git a/traffic_ops/traffic_ops_golang/steeringtargets/steeringtargets_test.go b/traffic_ops/traffic_ops_golang/steeringtargets/steeringtargets_test.go
index 2a79a232a..fe8fe0074 100644
--- a/traffic_ops/traffic_ops_golang/steeringtargets/steeringtargets_test.go
+++ b/traffic_ops/traffic_ops_golang/steeringtargets/steeringtargets_test.go
@@ -20,12 +20,13 @@ package steeringtargets
  */
 
 import (
+	"testing"
+
 	"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/jmoiron/sqlx"
 	"gopkg.in/DATA-DOG/go-sqlmock.v1"
-	"testing"
 )
 
 func TestInvalidSteeringTargetType(t *testing.T) {
@@ -82,7 +83,7 @@ func TestInvalidSteeringTargetType(t *testing.T) {
 		LastUpdated:            nil,
 	}
 
-	err = stObj.Validate()
+	err, _ = stObj.Validate()
 	if err == nil {
 		t.Fatal("expected user error to say that type is invalid, got no error instead")
 	}
diff --git a/traffic_ops/traffic_ops_golang/types/types_test.go b/traffic_ops/traffic_ops_golang/types/types_test.go
index 529417de5..3ad395401 100644
--- a/traffic_ops/traffic_ops_golang/types/types_test.go
+++ b/traffic_ops/traffic_ops_golang/types/types_test.go
@@ -181,7 +181,8 @@ func TestCreateInvalidType(t *testing.T) {
 
 func TestValidate(t *testing.T) {
 	p := TOType{}
-	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(p.Validate())))
+	err, _ := p.Validate()
+	errs := util.JoinErrsStr(test.SortErrors(test.SplitErrors(err)))
 	expected := util.JoinErrsStr(test.SortErrors([]error{
 		errors.New("'name' cannot be blank"),
 		errors.New("'description' cannot be blank"),