I’ve dug a little bit through Elm’s Kernel Code this weekend, and this seems to be a limitation / implementation detail of how the platform currently works.
Essentially, to send messages back to the update function, you need access to a Platform.Router value to pass to Platform.sendToApp. This value gets passed into the onEffects “update” function of effect managers. Now, the way to think about Cmd and Sub values in this context is “A Task bound to an effect manager” - which means when using Http.request, the request gets run by the Http effect manager, which can construct the underlying Task in a way such that the progress event handler has access to that Platform.Router, and can therefore send progress messages back to us.
When using Http.task >> Task.attempt instead, the Http module has to construct the task immediately without a Platform.Router, and that Task then gets run by the Task effect manager, which is more generic and has no notion of progress at all.